Что такое ошибка сегментации

Когда я делаю ошибку в коде, то обычно это приводит к появлению сообщения “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 <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
    int v = 3;
    printf("Code is at %p n", (void *)main);
    printf("Stack is at %p n", (void *)&v);
    printf("Heap is at %p n", malloc(8));

    return 0;
}

На моей машине 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/{pid}/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 бит для описания адресов (214 = 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. Также можете посмотреть, как на этой платформе осуществляется сегментирование. Вы откроете для себя интересные моменты относительно разбиения на страницы, дающие куда больше возможностей, чем при использовании классических сегментов.

From Wikipedia, the free encyclopedia

In computing, a segmentation fault (often shortened to segfault) or access violation is a fault, or failure condition, raised by hardware with memory protection, notifying an operating system (OS) the software has attempted to access a restricted area of memory (a memory access violation). On standard x86 computers, this is a form of general protection fault. The operating system kernel will, in response, usually perform some corrective action, generally passing the fault on to the offending process by sending the process a signal. Processes can in some cases install a custom signal handler, allowing them to recover on their own,[1] but otherwise the OS default signal handler is used, generally causing abnormal termination of the process (a program crash), and sometimes a core dump.

Segmentation faults are a common class of error in programs written in languages like C that provide low-level memory access and few to no safety checks. They arise primarily due to errors in use of pointers for virtual memory addressing, particularly illegal access. Another type of memory access error is a bus error, which also has various causes, but is today much rarer; these occur primarily due to incorrect physical memory addressing, or due to misaligned memory access – these are memory references that the hardware cannot address, rather than references that a process is not allowed to address.

Many programming languages may employ mechanisms designed to avoid segmentation faults and improve memory safety. For example, Rust employs an ownership-based[2] model to ensure memory safety.[3] Other languages, such as Lisp and Java, employ garbage collection,[4] which avoids certain classes of memory errors that could lead to segmentation faults.[5]

Overview[edit]

Example of human generated signal

Segmentation fault affecting Krita in KDE desktop environment

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).

The term «segmentation» has various uses in computing; in the context of «segmentation fault», a term used since the 1950s,[citation needed] it refers to the address space of a program.[6] With memory protection, only the program’s own address space is readable, and of this, only the stack and the read/write portion of the data segment of a program are writable, while read-only data and the code segment are not writable. Thus attempting to read outside of the program’s address space, or writing to a read-only segment of the address space, results in a segmentation fault, hence the name.

On systems using hardware memory segmentation to provide virtual memory, a segmentation fault occurs when the hardware detects an attempt to refer to a non-existent segment, or to refer to a location outside the bounds of a segment, or to refer to a location in a fashion not allowed by the permissions granted for that segment. On systems using only paging, an invalid page fault generally leads to a segmentation fault, and segmentation faults and page faults are both faults raised by the virtual memory management system. Segmentation faults can also occur independently of page faults: illegal access to a valid page is a segmentation fault, but not an invalid page fault, and segmentation faults can occur in the middle of a page (hence no page fault), for example in a buffer overflow that stays within a page but illegally overwrites memory.

At the hardware level, the fault is initially raised by the memory management unit (MMU) on illegal access (if the referenced memory exists), as part of its memory protection feature, or an invalid page fault (if the referenced memory does not exist). If the problem is not an invalid logical address but instead an invalid physical address, a bus error is raised instead, though these are not always distinguished.

At the operating system level, this fault is caught and a signal is passed on to the offending process, activating the process’s handler for that signal. Different operating systems have different signal names to indicate that a segmentation fault has occurred. On Unix-like operating systems, a signal called SIGSEGV (abbreviated from segmentation violation) is sent to the offending process. On Microsoft Windows, the offending process receives a STATUS_ACCESS_VIOLATION exception.

Causes[edit]

The conditions under which segmentation violations occur and how they manifest themselves are specific to hardware and the operating system: different hardware raises different faults for given conditions, and different operating systems convert these to different signals that are passed on to processes. The proximate cause is a memory access violation, while the underlying cause is generally a software bug of some sort. Determining the root cause – debugging the bug – can be simple in some cases, where the program will consistently cause a segmentation fault (e.g., dereferencing a null pointer), while in other cases the bug can be difficult to reproduce and depend on memory allocation on each run (e.g., dereferencing a dangling pointer).

The following are some typical causes of a segmentation fault:

  • Attempting to access a nonexistent memory address (outside process’s address space)
  • Attempting to access memory the program does not have rights to (such as kernel structures in process context)
  • Attempting to write read-only memory (such as code segment)

These in turn are often caused by programming errors that result in invalid memory access:

  • Dereferencing a null pointer, which usually points to an address that’s not part of the process’s address space
  • Dereferencing or assigning to an uninitialized pointer (wild pointer, which points to a random memory address)
  • Dereferencing or assigning to a freed pointer (dangling pointer, which points to memory that has been freed/deallocated/deleted)
  • A buffer overflow
  • A stack overflow
  • Attempting to execute a program that does not compile correctly. (Some compilers[which?] will output an executable file despite the presence of compile-time errors.)

In C code, segmentation faults most often occur because of errors in pointer use, particularly in C dynamic memory allocation. Dereferencing a null pointer, which results in undefined behavior, will usually cause a segmentation fault. This is because a null pointer cannot be a valid memory address. On the other hand, wild pointers and dangling pointers point to memory that may or may not exist, and may or may not be readable or writable, and thus can result in transient bugs. For example:

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

Dereferencing any of these variables could cause a segmentation fault: dereferencing the null pointer generally will cause a segfault, while reading from the wild pointer may instead result in random data but no segfault, and reading from the dangling pointer may result in valid data for a while, and then random data as it is overwritten.

Handling[edit]

The default action for a segmentation fault or bus error is abnormal termination of the process that triggered it. A core file may be generated to aid debugging, and other platform-dependent actions may also be performed. For example, Linux systems using the grsecurity patch may log SIGSEGV signals in order to monitor for possible intrusion attempts using buffer overflows.

On some systems, like Linux and Windows, it is possible for the program itself to handle a segmentation fault.[7] Depending on the architecture and operating system, the running program can not only handle the event but may extract some information about its state like getting a stack trace, processor register values, the line of the source code when it was triggered, memory address that was invalidly accessed[8] and whether the action was a read or a write.[9]

Although a segmentation fault generally means that the program has a bug that needs fixing, it is also possible to intentionally cause such failure for the purposes of testing, debugging and also to emulate platforms where direct access to memory is needed. On the latter case, the system must be able to allow the program to run even after the fault occurs. In this case, when the system allows, it is possible to handle the event and increment the processor program counter to «jump» over the failing instruction to continue the execution.[10]

Examples[edit]

Segmentation fault on an EMV keypad

Writing to read-only memory[edit]

Writing to read-only memory raises a segmentation fault. At the level of code errors, this occurs when the program writes to part of its own code segment or the read-only portion of the data segment, as these are loaded by the OS into read-only memory.

Here is an example of ANSI C code that will generally cause a segmentation fault on platforms with memory protection. It attempts to modify a string literal, which is undefined behavior according to the ANSI C standard. Most compilers will not catch this at compile time, and instead compile this to executable code that will crash:

int main(void)
{
    char *s = "hello world";
    *s = 'H';
}

When the program containing this code is compiled, the string «hello world» is placed in the rodata section of the program executable file: the read-only section of the data segment. When loaded, the operating system places it with other strings and constant data in a read-only segment of memory. When executed, a variable, s, is set to point to the string’s location, and an attempt is made to write an H character through the variable into the memory, causing a segmentation fault. Compiling such a program with a compiler that does not check for the assignment of read-only locations at compile time, and running it on a Unix-like operating system produces the following runtime error:

$ gcc segfault.c -g -o segfault
$ ./segfault
Segmentation fault

Backtrace of the core file from GDB:

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

This code can be corrected by using an array instead of a character pointer, as this allocates memory on stack and initializes it to the value of the string literal:

char s[] = "hello world";
s[0] = 'H';  // equivalently, *s = 'H';

Even though string literals should not be modified (this has undefined behavior in the C standard), in C they are of static char [] type,[11][12][13] so there is no implicit conversion in the original code (which points a char * at that array), while in C++ they are of static const char [] type, and thus there is an implicit conversion, so compilers will generally catch this particular error.

Null pointer dereference[edit]

In C and C-like languages, null pointers are used to mean «pointer to no object» and as an error indicator, and dereferencing a null pointer (a read or write through a null pointer) is a very common program error. The C standard does not say that the null pointer is the same as the pointer to memory address 0, though that may be the case in practice. Most operating systems map the null pointer’s address such that accessing it causes a segmentation fault. This behavior is not guaranteed by the C standard. Dereferencing a null pointer is undefined behavior in C, and a conforming implementation is allowed to assume that any pointer that is dereferenced is not null.

int *ptr = NULL;
printf("%d", *ptr);

This sample code creates a null pointer, and then tries to access its value (read the value). Doing so causes a segmentation fault at runtime on many operating systems.

Dereferencing a null pointer and then assigning to it (writing a value to a non-existent target) also usually causes a segmentation fault:

int *ptr = NULL;
*ptr = 1;

The following code includes a null pointer dereference, but when compiled will often not result in a segmentation fault, as the value is unused and thus the dereference will often be optimized away by dead code elimination:

Buffer overflow[edit]

The following code accesses the character array s beyond its upper boundary. Depending on the compiler and the processor, this may result in a segmentation fault.

char s[] = "hello world";
char c = s[20];

Stack overflow[edit]

Another example is recursion without a base case:

int main(void)
{
    return main();
}

which causes the stack to overflow which results in a segmentation fault.[14] Infinite recursion may not necessarily result in a stack overflow depending on the language, optimizations performed by the compiler and the exact structure of a code. In this case, the behavior of unreachable code (the return statement) is undefined, so the compiler can eliminate it and use a tail call optimization that might result in no stack usage. Other optimizations could include translating the recursion into iteration, which given the structure of the example function would result in the program running forever, while probably not overflowing its stack.

See also[edit]

  • General protection fault
  • Storage violation
  • Guru Meditation

References[edit]

  1. ^ Expert C programming: deep C secrets By Peter Van der Linden, page 188
  2. ^ «The Rust Programming Language — Ownership».
  3. ^ «Fearless Concurrency with Rust — The Rust Programming Language Blog».
  4. ^ McCarthy, John (April 1960). «Recursive functions of symbolic expressions and their computation by machine, Part I». Communications of the ACM. 4 (3): 184–195. doi:10.1145/367177.367199. S2CID 1489409. Retrieved 2018-09-22.
  5. ^ Dhurjati, Dinakar; Kowshik, Sumant; Adve, Vikram; Lattner, Chris (1 January 2003). «Memory Safety Without Runtime Checks or Garbage Collection» (PDF). Proceedings of the 2003 ACM SIGPLAN Conference on Language, Compiler, and Tool for Embedded Systems. ACM. 38 (7): 69–80. doi:10.1145/780732.780743. ISBN 1581136471. S2CID 1459540. Retrieved 2018-09-22.
  6. ^ «Debugging Segmentation Faults and Pointer Problems — Cprogramming.com». www.cprogramming.com. Retrieved 2021-02-03.
  7. ^ «Cleanly recovering from Segfaults under Windows and Linux (32-bit, x86)». Retrieved 2020-08-23.
  8. ^ «Implementation of the SIGSEGV/SIGABRT handler which prints the debug stack trace». GitHub. Retrieved 2020-08-23.
  9. ^ «How to identify read or write operations of page fault when using sigaction handler on SIGSEGV?(LINUX)». Retrieved 2020-08-23.
  10. ^ «LINUX – WRITING FAULT HANDLERS». Retrieved 2020-08-23.
  11. ^ «6.1.4 String literals». ISO/IEC 9899:1990 — Programming languages — C.
  12. ^ «6.4.5 String literals». ISO/IEC 9899:1999 — Programming languages — C.
  13. ^ «6.4.5 String literals». ISO/IEC 9899:2011 — Programming languages — C.
  14. ^ What is the difference between a segmentation fault and a stack overflow? at Stack Overflow

External links[edit]

  • Process: focus boundary and segmentation fault[dead link]
  • A FAQ: User contributed answers regarding the definition of a segmentation fault
  • A «null pointer» explained
  • Answer to: NULL is guaranteed to be 0, but the null pointer is not?
  • The Open Group Base Specifications Issue 6 signal.h

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

Конечно, случается эта проблема не только в Ubuntu, а во всех Linux дистрибутивах, поэтому наша инструкция будет актуальна для них тоже. Но сосредоточимся мы в основном на Ubuntu. Рассмотрим что такое ошибка сегментирования linux, почему она возникает, а также как с этим бороться и что делать.

Что такое ошибка сегментации?

Ошибка сегментации, Segmentation fault, или Segfault, или SIGSEGV в Ubuntu и других Unix подобных дистрибутивах, означает ошибку работы с памятью. Когда вы получаете эту ошибку, это значит, что срабатывает системный механизм защиты памяти, потому что программа попыталась получить доступ или записать данные в ту часть памяти, к которой у нее нет прав обращаться.

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

Допустим, в вашей системе есть 6 Гигабайт оперативной памяти, каждой программе нужно выделить определенную область, куда будет записана она сама, ее данные и новые данные, которые она будет создавать. Чтобы дать возможность каждой из запущенных программ использовать все шесть гигабайт памяти был придуман механизм виртуального адресного пространства. Создается виртуальное пространство очень большого размера, а из него уже выделяется по 6 Гб для каждой программы. Если интересно, это адресное пространство можно найти в файле /proc/kcore, только не вздумайте никуда его копировать.

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

Почему возникает ошибка сегментации?

И зачем бы это порядочной программе лезть, куда ей не положено? Да в принципе, незачем. Это происходит из-за ошибки при написании программ или несовместимых версиях библиотек и ПО. Часто эта ошибка встречается в программах на Си или C++. В этом языке программисты могут вручную работать с памятью, а язык со своей стороны не контролирует, чтобы они это делали правильно, поэтому одно неверное обращение к памяти может обрушить программу.

Почему может возникать эта ошибка при несовместимости библиотек? По той же причине — неверному обращению к памяти. Представим, что у нас есть библиотека linux (набор функций), в которой есть функция, которая выполняет определенную задачу. Для работы нашей функции нужны данные, поэтому при вызове ей нужно передать строку. Наша старая версия библиотеки ожидает, что длина строки будет до 256 символов. Но программа была обновлена формат записи поменялся, и теперь она передает библиотеке строку размером 512 символов. Если обновить программу, но оставить старую версию библиотеки, то при передаче такой строки 256 символов запишутся нормально в подготовленное место, а вот вторые 256 перезапишут данные программы, и возможно, попытаются выйти за пределы сегмента, тогда и будет ошибка сегментирования linux.

Что делать если возникла ошибка сегментирования?

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

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

Первое, что нужно сделать — это обновить систему до самой последней версии, возможно, был баг и его уже исправили, а может у вас установлены старые версии библиотек и обновление решит проблему. В Ubuntu это делается так:

sudo apt update
sudo apt full-upgrade

Если это не помогло, нужно обнулить настройки программы до значений по умолчанию, возможно, удалить кэш. Настройки программ в Linux обычно содержатся в домашней папке, скрытых подкаталогах с именем программы. Также, настройки и кэш могут содержаться в каталогах ~/.config и ~/.cache. Просто удалите папки программы и попробуйте снова ее запустить. Если и это не помогло, вы можете попробовать полностью удалить программу, а потом снова ее установить, возможно, какие-нибудь зависимости были повреждены:

sudo apt remove пакет_программы
sudo apt autoremove
sudo apt install пакет_программы

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

Когда вы все это выполнили, скорее всего, проблема не в вашем дистрибутиве, а в самой программе. Нужно отправлять отчет разработчикам. В Ubuntu это можно сделать с помощью программы apport-bug. Обычно Ubuntu предлагает это сделать сразу, после того как программа завершилась с ошибкой сегментирования. Если же ошибка сегментирования Ubuntu встречается не в системной программе, то вам придется самим искать разработчиков и вручную описывать что произошло.

Чтобы помочь разработчикам решить проблему, недостаточно отправить им только сообщение что вы поймали Segmentation Fault, нужно подробно описать проблему, действия, которые вы выполняли перед этим, так чтобы разработчик мог их воспроизвести. Также, желательно прикрепить к отчету последние функции, которые вызывала программа (стек вызовов функций), это может очень сильно помочь разработчикам.

Рассмотрим, как его получить. Это не так уж сложно. Сначала запустите вашу программу, затем узнайте ее PID с помощью команды:

pgrep программа

Дальше запускаем отладчик gdb:

sudo gdb -q

Подключаемся к программе:

(gdb) attach ваш_pid

После подключения программа станет на паузу, продолжаем ее выполнение командой:

(gdb) continue

segfault

Затем вам осталось только вызвать ошибку:

segfault1

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

(gdb) backtrace

Вывод этой команды и нужно отправлять разработчикам. Чтобы отключиться от программы и выйти наберите:

(gdb) detach
(gdb) quit

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

Выводы

Теперь у вас есть приблизительный план действий, что нужно делать, когда появляется ошибка сегментирования сделан дамп памяти ubuntu. Если вы знаете другие способы решить эту проблему, напишите в комментариях!

Обнаружили ошибку в тексте? Сообщите мне об этом. Выделите текст с ошибкой и нажмите Ctrl+Enter.

Creative Commons License

Статья распространяется под лицензией Creative Commons ShareAlike 4.0 при копировании материала ссылка на источник обязательна .

Когда я делаю ошибку в коде, то обычно это приводит к появлению сообщения “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 <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
    int v = 3;
    printf("Code is at %p n", (void *)main);
    printf("Stack is at %p n", (void *)&v);
    printf("Heap is at %p n", malloc(8));

    return 0;
}

На моей машине 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/{pid}/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 бит для описания адресов (214 = 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. Также можете посмотреть, как на этой платформе осуществляется сегментирование. Вы откроете для себя интересные моменты относительно разбиения на страницы, дающие куда больше возможностей, чем при использовании классических сегментов.

A segmentation fault (sometimes known as a segfault) happens when your program tries to access memory that it is not permitted to access.In other words, when your program attempts to access memory that exceeds the boundaries set by the operating system for your program.And it is a common circumstance that causes programs to crash; it is frequently related with a file called core.

Program memory is divided into different segments:

  • a text segment for program instructions
  • a data segment for variables and arrays defined at compile time
  • a stack segment for temporary (or automatic) variables defined in subroutines and functions
  • a heap segment for variables allocated during runtime by functions, such as malloc (in C) and allocate (in Fortran).

When a reference to a variable falls beyond the segment where that variable exists, or when a write is attempted to a place that is in a read-only segment, a segfault occurs. In reality, segfaults are nearly typically caused by attempting to read or write a non-existent array member, failing to correctly define a pointer before using it, or (in C applications) inadvertently using the value of a variable as an address (see the scan example below).

*Calling memset(), for example, would cause a program to segfault:

memset((char *)0x0, 1, 100);

*The three examples below show the most frequent sorts of array-related segfaults:

Case A

/* "Array out of bounds" error valid indices for array foo are 0, 1, ... 999 */
int foo[1000]; for (int i = 0; i <= 1000 ; i++) foo[i] = i;

Case B

/* Illegal memory access if value of n is not in the range 0, 1, ... 999 */ 
int n; int foo[1000]; for (int i = 0; i < n ; i++) foo[i] = i;

Case C

/* Illegal memory access because no memory is allocated for foo2 */
float *foo, *foo2; foo = (float*)malloc(1000); foo2[0] = 1.0;
  • In case A, array foo is defined for index = 0, 1, 2, … 999. However, in the last iteration of the for loop, the program tries to access foo[1000]. This will result in a segfault if that memory location lies outside the memory segment where foo resides. Even if it doesn’t cause a segfault, it is still a bug.
  • In case B, integer n could be any random value. As in case A, if it is not in the range 0, 1, … 999, it might cause a segfault. Whether it does or not, it is certainly a bug.
  • In case C, allocation of memory for variable foo2 has been overlooked, so foo2 will point to a random location in memory. Accessing foo2[0] will likely result in a segfault.

*Another typical programming issue that causes segfaults is a failure to use pointers properly. The C function scanf(), for example, requires the address of a variable as its second parameter; hence, the following will very certainly cause the program to fail with a segfault:

int foo = 0; scanf("%d", foo); 
/* Note missing & sign ; correct usage would have been &foo */

Although the variable foo may be created at memory position 1000, the preceding function call would attempt to read integer values into memory location 0 in accordance with the definition of foo.

A segfault occurs when a software attempts to operate on a memory region in an unauthorized manner (for example, attempts to write a read-only location would result in a segfault).When your application runs out of stack space, segfaults can occur. This might be due to your shell setting the stack size limit too low, rather than a fault in your software.

Dangling Pointers point to something that no longer exists.
A dangling pointer is an example of this.

char *ptr = NULL;
{
char c;
ptr = &c; //After the block is over, ptr will be a dangling pointer.
}

When the block concludes, the scope of variable c expires. Because it now points to something that doesn’t exist, the ‘ptr’ will become a dangling pointer.

But when you try to access memory that doesn’t belong to you or when you try to write to a read-only area, you get a segmentation fault.

char *str ="Testing Seg fault.";
*str= "I hate Seg fault :( ";

The’str’ will be made a constant by the compiler. You are altering the read-only part when you try to update the value, resulting in a segmentation fault.So there’s a clear distinction between a segmentation fault and dangling pointers.

A segmentation fault (sometimes known as a segfault) happens when your program tries to access memory that it is not permitted to access.In other words, when your program attempts to access memory that exceeds the boundaries set by the operating system for your program.And it is a common circumstance that causes programs to crash; it is frequently related with a file called core.

Program memory is divided into different segments:

  • a text segment for program instructions
  • a data segment for variables and arrays defined at compile time
  • a stack segment for temporary (or automatic) variables defined in subroutines and functions
  • a heap segment for variables allocated during runtime by functions, such as malloc (in C) and allocate (in Fortran).

When a reference to a variable falls beyond the segment where that variable exists, or when a write is attempted to a place that is in a read-only segment, a segfault occurs. In reality, segfaults are nearly typically caused by attempting to read or write a non-existent array member, failing to correctly define a pointer before using it, or (in C applications) inadvertently using the value of a variable as an address (see the scan example below).

*Calling memset(), for example, would cause a program to segfault:

memset((char *)0x0, 1, 100);

*The three examples below show the most frequent sorts of array-related segfaults:

Case A

/* "Array out of bounds" error valid indices for array foo are 0, 1, ... 999 */
int foo[1000]; for (int i = 0; i <= 1000 ; i++) foo[i] = i;

Case B

/* Illegal memory access if value of n is not in the range 0, 1, ... 999 */ 
int n; int foo[1000]; for (int i = 0; i < n ; i++) foo[i] = i;

Case C

/* Illegal memory access because no memory is allocated for foo2 */
float *foo, *foo2; foo = (float*)malloc(1000); foo2[0] = 1.0;
  • In case A, array foo is defined for index = 0, 1, 2, … 999. However, in the last iteration of the for loop, the program tries to access foo[1000]. This will result in a segfault if that memory location lies outside the memory segment where foo resides. Even if it doesn’t cause a segfault, it is still a bug.
  • In case B, integer n could be any random value. As in case A, if it is not in the range 0, 1, … 999, it might cause a segfault. Whether it does or not, it is certainly a bug.
  • In case C, allocation of memory for variable foo2 has been overlooked, so foo2 will point to a random location in memory. Accessing foo2[0] will likely result in a segfault.

*Another typical programming issue that causes segfaults is a failure to use pointers properly. The C function scanf(), for example, requires the address of a variable as its second parameter; hence, the following will very certainly cause the program to fail with a segfault:

int foo = 0; scanf("%d", foo); 
/* Note missing & sign ; correct usage would have been &foo */

Although the variable foo may be created at memory position 1000, the preceding function call would attempt to read integer values into memory location 0 in accordance with the definition of foo.

A segfault occurs when a software attempts to operate on a memory region in an unauthorized manner (for example, attempts to write a read-only location would result in a segfault).When your application runs out of stack space, segfaults can occur. This might be due to your shell setting the stack size limit too low, rather than a fault in your software.

Dangling Pointers point to something that no longer exists.
A dangling pointer is an example of this.

char *ptr = NULL;
{
char c;
ptr = &c; //After the block is over, ptr will be a dangling pointer.
}

When the block concludes, the scope of variable c expires. Because it now points to something that doesn’t exist, the ‘ptr’ will become a dangling pointer.

But when you try to access memory that doesn’t belong to you or when you try to write to a read-only area, you get a segmentation fault.

char *str ="Testing Seg fault.";
*str= "I hate Seg fault :( ";

The’str’ will be made a constant by the compiler. You are altering the read-only part when you try to update the value, resulting in a segmentation fault.So there’s a clear distinction between a segmentation fault and dangling pointers.

Материал из Национальной библиотеки им. Н. Э. Баумана
Последнее изменение этой страницы: 16:36, 24 августа 2017.

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

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

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

Общие понятия

Сегментная адресация памяти  — схема логической адресации памяти компьютера в архитектуре x86. Линейный адрес конкретной ячейки памяти, который в некоторых режимах работы процессора будет совпадать с физическим адресом, делится на две части: сегмент и смещение. Сегментом называется условно выделенная область адресного пространства определённого размера, а смещением — адрес ячейки памяти относительно начала сегмента. Базой сегмента называется линейный адрес (адрес относительно всего объёма памяти), который указывает на начало сегмента в адресном пространстве. В результате получается сегментный (логический) адрес, который соответствует линейному адресу база сегмента+смещение и который выставляется процессором на шину адреса.

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

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

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

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

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

На аппаратном уровне, неисправность возникает по причине реагирования блока управления памяти (MMU) на неправомерный доступ к памяти.

На уровне операционной системы эта ошибка ловится и сигнал передается в блок «offending process», где эта ошибка обрабатывается:

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

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

Причины сегментации

Наиболее распространенные причины ошибки сегментации:

  • Разыменование нулевых указателей 
  • Попытка доступа к несуществующему адресу памяти (за пределами адресного пространства процесса)
  • Попытка доступа к памяти программой, которая не имеет права на эту часть памяти
  • Попытка записи в память, предназначенной только для чтения

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

  • Создание операций с разыменованым указателем или создание неинициализированного указателя (указатель, который указывает на случайный адрес памяти)
  • Разыменование или возложение на освобожденной указатель (использование оборванного указателя, который указывает на память, которая была освобождена или перераспределена).
  • Переполнение буфера
  • Переполнение стека
  • Попытка выполнить программу, которая не компилируется правильно. (Некоторые компиляторы будут выводить исполняемый файл , несмотря на наличие ошибок во время компиляции.)

Пример Segmentation Fault

Рассмотрим пример кода на ANSI C, который приводит к ошибке сегментации вследствие присутствия квалификатора Сonst — type:

 const char *s = "hello world";
 *(char *)s = 'H';

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

Компиляция и запуск таких программ на OpenBSD 4.0 вызывает следующую ошибку выполнения:

 
$ 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';

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

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

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

 int* ptr = (int*)0;
 *ptr = 1;

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

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

 char* p1 = NULL;  /* инициализирован как нулевой, в чем нет ничего плохого, но на многих системах он не может быть разыменован */
 char* p2;  /* вообще не инициализирован */
 char* p3  = (char *)malloc(20);  /* хорошо, участок памяти выделен */
 free(p3);  /* но теперь его больше нет */

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

 int main()
 { 
     int const nmax=10;
     int i,n,a[n];
 }

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

Видеопример Segmentation Fault на примере C:

Источники

  • wiki info about Segmentation Fault
  • wiki Segmentation_fault
  • Почему возникает ошибка сегментации
  • Segmentation Fault
  • v
  • t
  • e

Операционные системы

Общая информация
  • Сравнение
  • Разработка криминалистического ПО
  • История
  • Список
  • Хронология
  • Доля использования
Ядро

Архитектура

  • Экзо
  • Гибридное
  • Микро
  • Монолитное
  • Нано

Компоненты

  • Драйвер
  • LKM
  • Микроядро
  • Пространство пользователя
Управление процессами

Аспекты

  • Переключение контекста
  • Прерывание
  • IPC
  • Процесс
  • Блок управления процессом
  • RTOS
  • Поток
  • Разделение времени

Планировщик
задач

  • Многозадачность
  • Упреждающее планирование с фиксированным приоритетом
  • Многоуровневые очереди с обратной связью
  • Вытесняющая многозадачность
  • Round-robin
  • SJN (Shortest job next)
Управление памятью
  • Ошибка на шине
  • Общая ошибка защиты
  • Защита памяти
  • Подкачка страниц
  • Кольца защиты
  • Ошибка сегментации
  • Виртуальная память
  • Страничная память
  • Сегментная адресация памяти
Память для хранения
Файловые системы
  • Загрузчик
  • Дефрагментация
  • Файл устройства
  • Атрибут файла
  • Inode
  • Журнал
  • Раздел диска
  • Виртуальная файловая система
  • VTL
Список
  • AmigaOS
  • Android
  • BeOS
  • BSD
  • Chrome OS
  • Cosmos
  • CP/M
  • Darwin
  • DOS
  • Genode
  • GNU
  • Haiku
  • illumos
  • IncludeOS
  • iOS
    • watchOS
    • tvOS
    • audioOS
  • ITS
  • Linux
  • Mac OS
    • Classic Mac OS
    • macOS
  • MINIX
  • MorphOS
  • MUSIC/SP
  • Nemesis
  • NeXTSTEP
  • NOS
  • OpenVMS
  • ORVYL
  • OS/2
  • OS-9
  • OSv
  • Pick
  • QNX
  • ReactOS
  • RISC OS
  • RSTS/E
  • RSX-11
  • RT-11
  • Solaris
  • TOPS-10/TOPS-20
  • z/TPF
  • TRIPOS
  • UNIX
  • Visi On
  • VM/CMS
  • VS/9
  • webOS
  • Windows
  • Xinu
  • z/OS
Прочее
  • API
  • Computer network
  • HAL
  • Live CD
  • Live USB
  • OS shell
    • CLI
    • GUI
    • NUI
    • TUI
    • VUI
    • ZUI
  • PXE

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

Конечно, случается эта проблема не только в Ubuntu, а во всех Linux дистрибутивах, поэтому наша инструкция будет актуальна для них тоже. Но сосредоточимся мы в основном на Ubuntu. Рассмотрим что такое ошибка сегментирования linux, почему она возникает, а также как с этим бороться и что делать.

Что такое ошибка сегментации?

Ошибка сегментации, Segmentation fault, или Segfault, или SIGSEGV в Ubuntu и других Unix подобных дистрибутивах, означает ошибку работы с памятью. Когда вы получаете эту ошибку, это значит, что срабатывает системный механизм защиты памяти, потому что программа попыталась получить доступ или записать данные в ту часть памяти, к которой у нее нет прав обращаться.

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

Допустим, в вашей системе есть 6 Гигабайт оперативной памяти, каждой программе нужно выделить определенную область, куда будет записана она сама, ее данные и новые данные, которые она будет создавать. Чтобы дать возможность каждой из запущенных программ использовать все шесть гигабайт памяти был придуман механизм виртуального адресного пространства. Создается виртуальное пространство очень большого размера, а из него уже выделяется по 6 Гб для каждой программы. Если интересно, это адресное пространство можно найти в файле /proc/kcore, только не вздумайте никуда его копировать.

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

Почему возникает ошибка сегментации?

И зачем бы это порядочной программе лезть, куда ей не положено? Да в принципе, незачем. Это происходит из-за ошибки при написании программ или несовместимых версиях библиотек и ПО. Часто эта ошибка встречается в программах на Си или C++. В этом языке программисты могут вручную работать с памятью, а язык со своей стороны не контролирует, чтобы они это делали правильно, поэтому одно неверное обращение к памяти может обрушить программу.

Почему может возникать эта ошибка при несовместимости библиотек? По той же причине — неверному обращению к памяти. Представим, что у нас есть библиотека linux (набор функций), в которой есть функция, которая выполняет определенную задачу. Для работы нашей функции нужны данные, поэтому при вызове ей нужно передать строку. Наша старая версия библиотеки ожидает, что длина строки будет до 256 символов. Но программа была обновлена формат записи поменялся, и теперь она передает библиотеке строку размером 512 символов. Если обновить программу, но оставить старую версию библиотеки, то при передаче такой строки 256 символов запишутся нормально в подготовленное место, а вот вторые 256 перезапишут данные программы, и возможно, попытаются выйти за пределы сегмента, тогда и будет ошибка сегментирования linux.

Что делать если возникла ошибка сегментирования?

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

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

Первое, что нужно сделать — это обновить систему до самой последней версии, возможно, был баг и его уже исправили, а может у вас установлены старые версии библиотек и обновление решит проблему. В Ubuntu это делается так:

sudo apt update
sudo apt full-upgrade

Если это не помогло, нужно обнулить настройки программы до значений по умолчанию, возможно, удалить кэш. Настройки программ в Linux обычно содержатся в домашней папке, скрытых подкаталогах с именем программы. Также, настройки и кэш могут содержаться в каталогах ~/.config и ~/.cache. Просто удалите папки программы и попробуйте снова ее запустить. Если и это не помогло, вы можете попробовать полностью удалить программу, а потом снова ее установить, возможно, какие-нибудь зависимости были повреждены:

sudo apt remove пакет_программы
sudo apt autoremove
sudo apt install пакет_программы

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

Когда вы все это выполнили, скорее всего, проблема не в вашем дистрибутиве, а в самой программе. Нужно отправлять отчет разработчикам. В Ubuntu это можно сделать с помощью программы apport-bug. Обычно Ubuntu предлагает это сделать сразу, после того как программа завершилась с ошибкой сегментирования. Если же ошибка сегментирования Ubuntu встречается не в системной программе, то вам придется самим искать разработчиков и вручную описывать что произошло.

Чтобы помочь разработчикам решить проблему, недостаточно отправить им только сообщение что вы поймали Segmentation Fault, нужно подробно описать проблему, действия, которые вы выполняли перед этим, так чтобы разработчик мог их воспроизвести. Также, желательно прикрепить к отчету последние функции, которые вызывала программа (стек вызовов функций), это может очень сильно помочь разработчикам.

Рассмотрим, как его получить. Это не так уж сложно. Сначала запустите вашу программу, затем узнайте ее PID с помощью команды:

pgrep программа

Дальше запускаем отладчик gdb:

sudo gdb -q

Подключаемся к программе:

(gdb) attach ваш_pid

После подключения программа станет на паузу, продолжаем ее выполнение командой:

(gdb) continue

segfault

Затем вам осталось только вызвать ошибку:

segfault1

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

(gdb) backtrace

Вывод этой команды и нужно отправлять разработчикам. Чтобы отключиться от программы и выйти наберите:

(gdb) detach
(gdb) quit

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

Выводы

Теперь у вас есть приблизительный план действий, что нужно делать, когда появляется ошибка сегментирования сделан дамп памяти ubuntu. Если вы знаете другие способы решить эту проблему, напишите в комментариях!

Creative Commons License

Статья распространяется под лицензией Creative Commons ShareAlike 4.0 при копировании материала ссылка на источник обязательна .

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

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

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

Пример

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

 const char *s = "hello world";
 *(char *)s = 'H';

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

Компиляция и запуск таких программ на OpenBSD 4.0 вызывает следующую ошибку выполнения:

$ 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';

} В отличие от этого, 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

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

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

 int* ptr = (int*)0;
 *ptr = 1;

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

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

Например,

    char* p1 = NULL;  /* инициализирован как нулевой, в чем нет ничего плохого, но на многих системах он не может быть разыменован */
    char* p2;  /* вообще не инициализирован */
    char* p3  = (char *)malloc(20);  /* хорошо, участок памяти выделен */
    free(p3);  /* но теперь его больше нет */

Теперь разыменование любого из этих указателей может вызвать ошибку сегментации.

Также, при использовании массивов, если случайно указать вместо максимального размера массива его текущий. Вот пример :

int main()
{ int const nmax=10;
  int i,n,a[n];
}

Проблемы с содержанием статьи

Проверить информацию.

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

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

См. также

  • Переполнение буфера

Ссылки

  • http://www.faqs.org/qa/qa-673.html  (англ.)
  • http://c-faq.com/null/null1.html  (англ.)
  • http://c-faq.com/null/varieties.html  (англ.)
 Просмотр этого шаблона Аспекты операционных систем (история • список)
Ядро

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

Управление
процессами

Режимы (супервизора • реальный • защищённый) • Прерывание • Кольца защиты • Переключение контекста • Многозадачность (вытесняющая • кооперативная • мультипрограммирование) • Процесс • Управление процессом • Планировщик задач • Многопоточность

Управление
памятью

Защита памяти • Сегментная адресация памяти • Страничная память • Менеджер виртуальной памяти • Ошибка сегментации Общая ошибка защиты

Прочее

Загрузчик ОС • API • VFS • Компьютерная сеть • GUI • Слой аппаратных абстракций (HAL)

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

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

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

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

  • Каков самый простой стандартный способ создания Segfault в 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 оператор в условном выражении, предназначенный для предотвращения бесконечной рекурсии в функции. Мораль этой истории, всегда проверяйте исправность ошибок!

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

  • Ошибка сегментации при создании больших массивов в C
  • Ошибка сегмента при инициализации массива

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

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

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

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

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

  • Что означает «дикий указатель» в C?

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

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

Если вы читаете за концом массива, вы можете оказаться в памяти, которая не инициализирована или принадлежит чему-то другому. Это технически неопределенное поведение. 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);

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

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

  • У меня есть segfaults!

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

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

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

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

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


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

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

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

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

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

  • Почему этот C-код обращения строки вызывает ошибку сегментации?

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

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

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

  • Поведение malloc с удалением в C ++
  • Ошибка сегментации (ядро сброшено), когда я удаляю указатель

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

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

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

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


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

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

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

  • UD2 генерируется на некоторых платформах из-за других UB
  • c ++ STL map :: operator [] сделано для удаляемой записи

DEBUGGING

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

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

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

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

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

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

64

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

Неправильная строка управления форматом в операторах printf или scanf

Строка управления форматом должна иметь одинаковое количество спецификаторов преобразования (%s, %d и т.д.), поскольку printf или scanf имеют аргументы для печати или чтения. То же самое относится к fprintf и fscanf.

Не использовать & для аргументов scanf

Функция scanf принимает в качестве аргументов строку управления форматом и адреса переменных, в которые она поместит данные, которые она читает. Оператор & (адрес) используется для указания адреса переменной.

Ссылки на массив вне границ

Убедитесь, что вы не нарушили границы любого массива, который вы используете; т.е. вы не индексировали массив со значением, меньшим, чем индекс его самого нижнего элемента, или больше, чем индекс его самого высокого элемента. Valgrind может оказаться полезным для обнаружения таких ссылок — вы можете использовать Valgrind с флагом --tool=exp-sgcheck.

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

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

Неправильное использование операторов & (адрес) и * (разыменование)

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

Ограничения оболочки

Иногда ошибки сегментации не вызваны ошибками в программе, а вызваны тем, что ограничения системной памяти установлены слишком низко. Обычно это ограничение на размер стека вызывает такую ​​проблему (переполнение стека). Чтобы проверить пределы памяти, используйте команду ulimit в bash.

Отладка с использованием gdb

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

A segmentation fault (sometimes known as a segfault) happens when your program tries to access memory that it is not permitted to access.In other words, when your program attempts to access memory that exceeds the boundaries set by the operating system for your program.And it is a common circumstance that causes programs to crash; it is frequently related with a file called core.

Program memory is divided into different segments:

  • a text segment for program instructions
  • a data segment for variables and arrays defined at compile time
  • a stack segment for temporary (or automatic) variables defined in subroutines and functions
  • a heap segment for variables allocated during runtime by functions, such as malloc (in C) and allocate (in Fortran).

When a reference to a variable falls beyond the segment where that variable exists, or when a write is attempted to a place that is in a read-only segment, a segfault occurs. In reality, segfaults are nearly typically caused by attempting to read or write a non-existent array member, failing to correctly define a pointer before using it, or (in C applications) inadvertently using the value of a variable as an address (see the scan example below).

*Calling memset(), for example, would cause a program to segfault:

memset((char *)0x0, 1, 100);

*The three examples below show the most frequent sorts of array-related segfaults:

Case A

/* "Array out of bounds" error valid indices for array foo are 0, 1, ... 999 */
int foo[1000]; for (int i = 0; i <= 1000 ; i++) foo[i] = i;

Case B

/* Illegal memory access if value of n is not in the range 0, 1, ... 999 */ 
int n; int foo[1000]; for (int i = 0; i < n ; i++) foo[i] = i;

Case C

/* Illegal memory access because no memory is allocated for foo2 */
float *foo, *foo2; foo = (float*)malloc(1000); foo2[0] = 1.0;
  • In case A, array foo is defined for index = 0, 1, 2, … 999. However, in the last iteration of the for loop, the program tries to access foo[1000]. This will result in a segfault if that memory location lies outside the memory segment where foo resides. Even if it doesn’t cause a segfault, it is still a bug.
  • In case B, integer n could be any random value. As in case A, if it is not in the range 0, 1, … 999, it might cause a segfault. Whether it does or not, it is certainly a bug.
  • In case C, allocation of memory for variable foo2 has been overlooked, so foo2 will point to a random location in memory. Accessing foo2[0] will likely result in a segfault.

*Another typical programming issue that causes segfaults is a failure to use pointers properly. The C function scanf(), for example, requires the address of a variable as its second parameter; hence, the following will very certainly cause the program to fail with a segfault:

int foo = 0; scanf("%d", foo); 
/* Note missing & sign ; correct usage would have been &foo */

Although the variable foo may be created at memory position 1000, the preceding function call would attempt to read integer values into memory location 0 in accordance with the definition of foo.

A segfault occurs when a software attempts to operate on a memory region in an unauthorized manner (for example, attempts to write a read-only location would result in a segfault).When your application runs out of stack space, segfaults can occur. This might be due to your shell setting the stack size limit too low, rather than a fault in your software.

Dangling Pointers point to something that no longer exists.
A dangling pointer is an example of this.

char *ptr = NULL;
{
char c;
ptr = &c; //After the block is over, ptr will be a dangling pointer.
}

When the block concludes, the scope of variable c expires. Because it now points to something that doesn’t exist, the ‘ptr’ will become a dangling pointer.

But when you try to access memory that doesn’t belong to you or when you try to write to a read-only area, you get a segmentation fault.

char *str ="Testing Seg fault.";
*str= "I hate Seg fault :( ";

The’str’ will be made a constant by the compiler. You are altering the read-only part when you try to update the value, resulting in a segmentation fault.So there’s a clear distinction between a segmentation fault and dangling pointers.

Содержание:

  • Handling
  • Overview
  • Causes
  • Что такое ошибка сегментации?
  • Additional Information
  • Examples
    • Writing to read-only memory
    • Null pointer dereference
    • Stack overflow

Handling

The default action for a segmentation fault or bus error is abnormal termination of the process that triggered it. A core file may be generated to aid debugging, and other platform-dependent actions may also be performed. For example, Linux systems using the grsecurity patch may log SIGSEGV signals in order to monitor for possible intrusion attempts using buffer overflows.

On some systems, like Linux and Windows, it is possible for the program itself to handle a segmentation fault.. Depending on the architecture and operating system, the running program can not only handle the event but may extract some information about its state like getting a stack trace, processor register values, the line of the source code when it was triggered, memory address that was invalidly accessed and whether the action was a read or a write.

Although a segmentation fault generally means that the program has a bug that needs fixing, it is also possible to intentionally cause such failure for the purposes of testing, debugging and also to emulate platforms were direct access to memory is needed. On the latter case, the system must be able to allow the program to run even after the fault occurs. In this case, when the system allows, it is possible to handle the event and increment the processor program counter to «jump» over the failing instruction to continue the execution.

Overview

Example of human generated signal

A null pointer dereference on Windows 8

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).

The term «segmentation» has various uses in computing; in the context of «segmentation fault», a term used since the 1950s, it refers to the address space of a program.[citation needed] With memory protection, only the program’s own address space is readable, and of this, only the stack and the read/write portion of the data segment of a program are writable, while read-only data and the code segment are not writable. Thus attempting to read outside of the program’s address space, or writing to a read-only segment of the address space, results in a segmentation fault, hence the name.

On systems using hardware memory segmentation to provide virtual memory, a segmentation fault occurs when the hardware detects an attempt to refer to a non-existent segment, or to refer to a location outside the bounds of a segment, or to refer to a location in a fashion not allowed by the permissions granted for that segment. On systems using only paging, an invalid page fault generally leads to a segmentation fault, and segmentation faults and page faults are both faults raised by the virtual memory management system. Segmentation faults can also occur independently of page faults: illegal access to a valid page is a segmentation fault, but not an invalid page fault, and segmentation faults can occur in the middle of a page (hence no page fault), for example in a buffer overflow that stays within a page but illegally overwrites memory.

At the hardware level, the fault is initially raised by the memory management unit (MMU) on illegal access (if the referenced memory exists), as part of its memory protection feature, or an invalid page fault (if the referenced memory does not exist). If the problem is not an invalid logical address but instead an invalid physical address, a bus error is raised instead, though these are not always distinguished.

At the operating system level, this fault is caught and a signal is passed on to the offending process, activating the process’s handler for that signal. Different operating systems have different signal names to indicate that a segmentation fault has occurred. On Unix-like operating systems, a signal called SIGSEGV (abbreviated from segmentation violation) is sent to the offending process. On Microsoft Windows, the offending process receives a STATUS_ACCESS_VIOLATION exception.

Causes

The conditions under which segmentation violations occur and how they manifest themselves are specific to hardware and the operating system: different hardware raises different faults for given conditions, and different operating systems convert these to different signals that are passed on to processes. The proximate cause is a memory access violation, while the underlying cause is generally a software bug of some sort. Determining the root cause – debugging the bug – can be simple in some cases, where the program will consistently cause a segmentation fault (e.g., dereferencing a null pointer), while in other cases the bug can be difficult to reproduce and depend on memory allocation on each run (e.g., dereferencing a dangling pointer).

The following are some typical causes of a segmentation fault:

  • Attempting to access a nonexistent memory address (outside process’s address space)
  • Attempting to access memory the program does not have rights to (such as kernel structures in process context)
  • Attempting to write read-only memory (such as code segment)

These in turn are often caused by programming errors that result in invalid memory access:

  • Dereferencing a null pointer, which usually points to an address that’s not part of the process’s address space
  • Dereferencing or assigning to an uninitialized pointer (wild pointer, which points to a random memory address)
  • Dereferencing or assigning to a freed pointer (dangling pointer, which points to memory that has been freed/deallocated/deleted)
  • A buffer overflow
  • A stack overflow
  • Attempting to execute a program that does not compile correctly. (Some compilers will output an executable file despite the presence of compile-time errors.)

In C code, segmentation faults most often occur because of errors in pointer use, particularly in C dynamic memory allocation. Dereferencing a null pointer will always result in a segmentation fault, but wild pointers and dangling pointers point to memory that may or may not exist, and may or may not be readable or writable, and thus can result in transient bugs. For example:

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

Now, dereferencing any of these variables could cause a segmentation fault: dereferencing the null pointer generally will cause a segfault, while reading from the wild pointer may instead result in random data but no segfault, and reading from the dangling pointer may result in valid data for a while, and then random data as it is overwritten.

Что такое ошибка сегментации?

Ошибка сегментации, Segmentation fault, или Segfault, или SIGSEGV в Ubuntu и других Unix подобных дистрибутивах, означает ошибку работы с памятью. Когда вы получаете эту ошибку, это значит, что срабатывает системный механизм защиты памяти, потому что программа попыталась получить доступ или записать данные в ту часть памяти, к которой у нее нет прав обращаться.

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

Допустим, в вашей системе есть 6 Гигабайт оперативной памяти, каждой программе нужно выделить определенную область, куда будет записана она сама, ее данные и новые данные, которые она будет создавать. Чтобы дать возможность каждой из запущенных программ использовать все шесть гигабайт памяти был придуман механизм виртуального адресного пространства. Создается виртуальное пространство очень большого размера, а из него уже выделяется по 6 Гб для каждой программы. Если интересно, это адресное пространство можно найти в файле /proc/kcore, только не вздумайте никуда его копировать.

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

Additional Information

BackgroundModern general purpose hardware includes a «memory management unit» (or MMU). This hardware feature is used by operating systems like Linux to implement memory protection, i.e. to prevent different processes from accessing or modifying each other’s memory (except in a strictly controlled fashion through specific APIs). This simplifies troubleshooting and increases resilience as processes are carefully contained and separated from one another.A «segmentation violation» signal is sent to a process of which the memory management unit detected an attempt to use a memory address that does not belong to it.Common cause: programming errorIf a process tries to access memory through a pointer which has not been properly initialised, or which is pointing to memory that has previously been deallocated, this will likely result in a segfault. In this case, the segfault is occurring with a specific process or binary, possibly under a specific set of circumstances.To deal with this type of segfault, begin by applying all relevant service packs and maintenance updates. Next, if the problem is still reproducible with current code, try to capture an application core dump for analysis. Details for this are found in TID 3054866 — How to obtain application core dumps.If the problematic binary is shipped by Novell as part of the product, open a service request with Novell Technical services, supplying

  • a description of the circumstances under which the core dump was generated,
  • a supportconfig -v report for the affected system (supportconfig home page), and
  • the core dump file itself, if smaller than 50 MB. If it is larger, wait for the service request to be assigned to an engineer and then work with the engineer to transfer it.

If the problematic binary was supplied by a third party, contact that party’s support department for assistance.Common cause: mismatched binary and librariesSegfaults can occur with processes that combine binaries and shared libraries which aren’t (fully) compatible. This can occur, for instance, when a library is updated in a way that changes the library’s ABI (application binary interface), but the library’s internal version number is not updated to reflect this. A binary that was built against an older version of the library may start to segfault when loaded against the newer version.This case may be difficult to recognise, and there is no step list for dealing with it. When this case is suspected, some things to consider are:

  • Checking the system for file corruption (e.g. using the rpm-verify.txt file in a supportconfig -v report)
  • Checking the system’s settings that relate to shared libraries, like the /etc/ld.so.conf and /etc/ld.so.conf.d/* configuration files and the LD_LIBRARY_PATH and LD_PRELOAD environment variables.
  • Using the objdump tool to check the binary for a built-in library search path override (RPATH).

Common cause: hardware or hardware configuration issueWhen segfaults are occurring frequently, or with different processes or without a clear pattern to them, this can indicate that a system’s hardware (memory subsystem) is problematic, or that low-level system configuration settings are inappropriate. Please refer to TID 3301593 — Linux system hangs or is unstable for more information on how to handle this situation.

+ Upgrading to Open Enterprise Server 2 SP1 Linux

+ Open Enterprise Server 2 SP1 Migration Strategies

Examples

Segmentation fault on an EMV keypad

Writing to read-only memory

Writing to read-only memory raises a segmentation fault. At the level of code errors, this occurs when the program writes to part of its own code segment or the read-only portion of the data segment, as these are loaded by the OS into read-only memory.

Here is an example of ANSI C code that will generally cause a segmentation fault on platforms with memory protection. It attempts to modify a string literal, which is undefined behavior according to the ANSI C standard. Most compilers will not catch this at compile time, and instead compile this to executable code that will crash:

int main(void)
{
    char *s = "hello world";
    *s = 'H';
}

When the program containing this code is compiled, the string «hello world» is placed in the rodata section of the program executable file: the read-only section of the data segment. When loaded, the operating system places it with other strings and constant data in a read-only segment of memory. When executed, a variable, s, is set to point to the string’s location, and an attempt is made to write an H character through the variable into the memory, causing a segmentation fault. Compiling such a program with a compiler that does not check for the assignment of read-only locations at compile time, and running it on a Unix-like operating system produces the following runtime error:

$ gcc segfault.c -g -o segfault
$ ./segfault
Segmentation fault

Backtrace of the core file from GDB:

Program received signal SIGSEGV, Segmentation fault.
0x1c0005c2 in main () at segfault.c6
6               *s = 'H';

This code can be corrected by using an array instead of a character pointer, as this allocates memory on stack and initializes it to the value of the string literal:

char s[] = "hello world";
s = 'H';  // equivalently, *s = 'H';

Even though string literals should not be modified (this has undefined behavior in the C standard), in C they are of type, so there is no implicit conversion in the original code (which points a at that array), while in C++ they are of type, and thus there is an implicit conversion, so compilers will generally catch this particular error.

Null pointer dereference

In C and C-like languages, null pointers are used to mean «pointer to no object» and as an error indicator, and dereferencing a null pointer (a read or write through a null pointer) is a very common program error. The C standard does not say that the null pointer is the same as the pointer to memory address 0, though that may be the case in practice. Most operating systems map the null pointer’s address such that accessing it causes a segmentation fault. This behavior is not guaranteed by the C standard. Dereferencing a null pointer is undefined behavior in C, and a conforming implementation is allowed to assume that any pointer that is dereferenced is not null.

int *ptr = NULL;
printf("%d", *ptr);

This sample code creates a null pointer, and then tries to access its value (read the value). Doing so causes a segmentation fault at runtime on many operating systems.

Dereferencing a null pointer and then assigning to it (writing a value to a non-existent target) also usually causes a segmentation fault:

int *ptr = NULL;
*ptr = 1;

The following code includes a null pointer dereference, but when compiled will often not result in a segmentation fault, as the value is unused and thus the dereference will often be optimized away by dead code elimination:

int *ptr = NULL;
*ptr;

Stack overflow

Another example is recursion without a base case:

int main(void)
{
    main();
    return ;
}

which causes the stack to overflow which results in a segmentation fault. Infinite recursion may not necessarily result in a stack overflow depending on the language, optimizations performed by the compiler and the exact structure of a code. In this case, the behavior of unreachable code (the return statement) is undefined, so the compiler can eliminate it and use a tail call optimization that might result in no stack usage. Other optimizations could include translating the recursion into iteration, which given the structure of the example function would result in the program running forever, while probably not overflowing its stack.

Ошибка компьютера вызвано доступ к ограниченной памяти

В вычислениях ошибка сегментации (часто сокращается до segfault ) или нарушение доступа . сбой или состояние отказа, вызванное аппаратным обеспечением с защитой памяти, уведомляющее операционную систему (ОС) о том, что программное обеспечение пыталось получить доступ к ограниченной области памяти (нарушение доступа к памяти). На стандартных компьютерах x86 это форма общей ошибки защиты. Ядро ОС в ответ обычно выполняет некоторые корректирующие действия, обычно передавая неисправность вызывающему ошибку процессу , отправляя процессу сигнал . В некоторых случаях процессы могут устанавливать собственный обработчик сигналов, позволяя им восстанавливаться самостоятельно, но в противном случае используется обработчик сигналов ОС по умолчанию, что обычно вызывает аварийное завершение процесса (программа сбой ), а иногда и дамп ядра.

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

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

Содержание

  • 1 Обзор
  • 2 Причины
  • 3 Обработка
  • 4 Примеры
    • 4.1 Запись в постоянную память
    • 4.2 Разыменование нулевого указателя
    • 4.3 Переполнение буфера
    • 4.4 Переполнение стека
  • 5 См. Также
  • 6 Ссылки
  • 7 Внешние ссылки

Обзор

Пример сигнала, сгенерированного человеком A нулевой указатель разыменование в Windows 8

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

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

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

На аппаратном уровне ошибка изначально возникает из-за несанкционированного доступа блоком управления памятью (MMU) (если указанная память существует) как часть его функции защиты памяти, или ошибка неверной страницы (если указанная память не существует). Если проблема заключается не в неверном логическом адресе, а в недопустимом физическом адресе, вместо этого возникает ошибка шины , хотя они не всегда распознаются.

На уровне операционной системы эта ошибка перехватывается, и сигнал передается процессу-нарушителю, активируя обработчик процесса для этого сигнала. В разных операционных системах используются разные имена сигналов, указывающие на ошибку сегментации. В Unix-подобных операционных системах сигнал, называемый SIGSEGV (сокращенно от нарушения сегментации), отправляется нарушившему процессу. В Microsoft Windows нарушающий процесс получает исключение STATUS_ACCESS_VIOLATION .

Причины

Условия, при которых происходят нарушения сегментации и как они проявляются, зависят от оборудования и операционной системы. : различное оборудование вызывает разные сбои для данных условий, и разные операционные системы преобразуют их в разные сигналы, которые передаются процессам. Непосредственной причиной является нарушение доступа к памяти, а основной причиной обычно является программная ошибка того или иного рода. Определение основной причины — отладка ошибки — может быть простым в некоторых случаях, когда программа постоянно вызывает ошибку сегментации (например, разыменование нулевого указателя ), в то время как в других случаях ошибку может быть трудно воспроизвести и она зависит от распределения памяти при каждом запуске (например, разыменование висячего указателя ).

Ниже приведены некоторые типичные причины ошибки сегментации:

  • Попытка доступа к несуществующему адресу памяти (вне адресного пространства процесса)
  • Попытка доступа к памяти, на которую программа не имеет прав (например, структуры ядра в контексте процесса)
  • Попытка записи в память только для чтения (например, сегмент кода)

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

  • Разыменование нулевого указателя, который обычно указывает на адрес, не являющийся частью адресного пространства процесса
  • Разыменование или присвоение неинициализированному указателю (дикий указатель, который указывает к случайному адресу памяти)
  • Разыменование или присвоение освобожденному указателю (висячий указатель, который указывает на память, которая была освобождена / освобождена / удалена)
  • A переполнение буфера
  • A stack overflow
  • Попытка выполнить некорректную компиляцию программы. (Некоторые компиляторы выводят исполняемый файл, несмотря на наличие ошибок времени компиляции.)

В коде C сбои сегментации чаще всего возникают из-за ошибок при использовании указателя, особенно в C dynamic выделение памяти. Разыменование нулевого указателя всегда будет приводить к ошибке сегментации, но дикие указатели и висячие указатели указывают на память, которая может существовать, а может и не существовать, и может быть или не быть доступной для чтения или записи, и, таким образом, может привести к временным ошибкам. Например:

char * p1 = NULL; // Нулевой указатель char * p2; // Дикий указатель: вообще не инициализирован. char * p3 = malloc (10 * sizeof (char)); // Инициализированный указатель на выделенную память // (при условии, что malloc не завершился ошибкой) free (p3); // p3 теперь является висящим указателем, так как память была освобождена

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

Обработка

Действие по умолчанию для ошибки сегментации или ошибки шины — аварийное завершение процесса, который ее инициировал. Для облегчения отладки может быть сгенерирован файл core, а также могут быть выполнены другие действия, зависящие от платформы. Например, системы Linux, использующие патч grsecurity, могут регистрировать сигналы SIGSEGV для отслеживания возможных попыток вторжения с помощью переполнения буфера.

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

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

Примеры

Ошибка сегментации на EMV клавиатура

Запись в постоянную память

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

Вот пример кода 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 Ошибка сегментации

Отслеживание основного файла из GDB :

Программа получила сигнал SIGSEGV, Ошибка сегментации. 0x1c0005c2 в main () на segfault.c: 6 6 * s = 'H';

Этот код можно исправить, используя массив вместо указателя на символ, так как он выделяет память в стеке и инициализирует ее значением строкового литерала:

char s = "hello world"; s [0] = 'H'; // эквивалентно * s = 'H';

Несмотря на то, что строковые литералы не должны изменяться (в стандарте C это имеет неопределенное поведение), в C они имеют тип static char, поэтому в исходном коде нет неявного преобразования (что указывает на a 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;

Переполнение буфера

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

Другой пример — рекурсия без базового случая:

int main (void) {main (); возврат 0; }

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

См. Также

  • Дамп ядра
  • Общая ошибка защиты
  • Ошибка страницы
  • Нарушение памяти

Ссылки

Внешние ссылки

  • Процесс: граница фокуса и ошибка сегментации
  • Часто задаваемые вопросы: ответы пользователей относительно определения ошибки сегментации
  • «нулевой указатель» «объяснено
  • Ответ на: NULL гарантированно равен 0, но нулевой указатель не равен?
  • Проблема 6 основных спецификаций открытой группы signal.h

Понравилась статья? Поделить с друзьями:
  • Что такое ошибка свс карты
  • Что такое ошибка реестра на компьютере
  • Что такое ошибка распаковки
  • Что такое ошибка разрешения сетевого имени
  • Что такое ошибка пыли utxo