Segfault error linux

Когда я делаю ошибку в коде, то обычно это приводит к появлению сообщения “segmentation fault”, зачастую сокращённого до “segfault”. И тут же мои коллеги и рук...

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

Не всегда программы в 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 при копировании материала ссылка на источник обязательна .

Understanding and solving errors are essential skills for any Linux administrator. The most common errors you will get are: “no such file or directory found” or “unable to access or permission denied”. If you are a Linux administrator, it is mandatory to know how to detect and solve segmentation faults. In this article, we will explain what segmentation fault is, what causes them, and how to detect or troubleshoot them. So let’s get started.

What Is a Segmentation Fault?

A segmentation fault is nothing but the error that occurs due to failed attempts to access Linux OS’s memory regions. These types of faults are detected by the kernel. Once detected, the process is immediately terminated, and a “segmentation violation signal” or “segmentation fault” is issued. 

We can find most segmentation faults in lower-level languages like C (the most commonly used/ fundamental language in both LINUX and UNIX). It allows a great deal on memory allocation and usage. Hence, developers can have full control over the memory allocation. 

What Causes a Segmentation Fault? 

In the Linux or kernel operating system, the following are the conditions that cause a segmentation fault:

  • Segmentation Violation Mapping Error (SEGV_MAPERR): This is the error that occurs when you want to access memory outside the application that requires an address space. 
  • Segmentation Violation Access Error (SEGV_ACCERR): This is the error that occurs when you want to access memory where the application does not have permission or write source codes on read-only memory space.

Sometimes we assume that these two conditions cause major problems, but that’s not always true. There might be chances of getting errors through referencing NULL values, freed pointers available for the reference memory areas, non-initialized parameters, and StackOverflow.

Examples That Generate Segmentation Faults in Linux

Here, we are going to explain a few code snippets that generate the segmentation default in Linux:

void main (void) {
   char *buffer;  /* Non initialized buffer */
   buffer[0] = 0; /* Trying to assign 0 to its first position will cause a segmentation fault */
}

We can now run and compile them on Linux kernel as follows:

$ gcc -o seg_fault -ggdb seg_fault.c

Output:

Segmentation fault (core dumped)

In the above example, ulimit dumps the process memory on errors, and the compiling done with the help of GCC or -ggtb options, adds debug information on the resulting binary.

In addition, we can also enable get debug information and core dumping where the error occurred as shown in the below example:

$ gdb ./seg_fault /var/crash/core.seg_fault

Output:

... <snip> ...
Reading symbols from ./seg_fault...
[New LWP 6291]
Core was generated by `./seg_fault'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x000055ea4064c135 in main () at seg_fault.c:4
4               buffer[0] = 0;

Note: However, if you don’t have to debug information, you can still identify the errors with the help of the name of the function, and values where we are getting errors.

How to Detect a Segmentation Fault

Till now, we have seen a few examples where we get errors. In this section, we will explain how to diagnose/detect the segmentation fault.

Using a debugger, you might detect segmentation faults. There are various kinds of debuggers available, but the most often used debuggers are GDB or -ggtb. GDB is a well-known GNU debugger that helps us view the backtrace of the core file, which is dumped by the program. 

Whenever a segmentation fault occurs in the program, it usually dumps the memory content at the time of the core file crash process. Start your debugger, GDB, with the “gdb core” command. Then, use the “backtrace” command to check for the program crash. 

If you are unable to detect the segmentation fault using the above solution, try to run your program under “debugger” control. Then, go through the source code, one code line or one code function at a time. To perform this task, all you need to do is compile your program codes without optimization, and use the “-g” flag to find out the errors. 

Let us explain the simple steps required to identify segFault inside GDB:

  1. Make sure you have enabled the debugger mode (-g flag should also be a part of the GCC line). It looks like this:

gcc -g -o segfault segfault.c

  1. Then load the file into the gdb format:

  1. For the sample code, use this navigation to identify the codes that cause SegFault:

  1. Then, run your program: Starting program: /home/pi/segfault

program received signal SIGSEGV, Segmentation fault.

The following image illustrates the above steps:

Identify segFault inside GDB illustration

How to Prevent Segmentation Faults in Linux?

When you write a program using pointers, memory allocators, or references, make sure that all the memory accessories are within the boundary and compile with proper access restrictions. 

The below are the important things which you should take care of them to avoid segmentation faults:

  • Using gdb debugger to track the source of the problem while using dynamic memory location.
  • Make sure that you have installed or configured the correct hardware or software components.
  • Maintaining type consistency throughout the program code and the function parameters that call convention values will reduce the Segfault.
  • Always make sure that all operating system dependencies are installed inside the jail.
  • Stop using conditional statements in recursive functions.
  • Turn on the core dumping support services (especially Apache) to prevent the segmentation fault in the Linux program.

These tips are very important as they improve code robustness and security services. 

Final Take

If you are a Linux developer, you might have gone through the segmentation fault scenarios many times. In this post, we have tried to brief you about SegFault, given real-time examples, explained the causes for the SegFault, and discussed how to detect and prevent segmentation faults. 

We hope this blog may help a few Linux communities, Linux administrators, and Linux experts worldwide who want to master the core concepts of the Linux kernel system. 

Fault (technology)
Linux kernel
Segmentation fault
Memory (storage engine)
shell
operating system

Opinions expressed by DZone contributors are their own.

When running applications, whether it’s on your office desktop, home computer, or mobile device, you just expect them to work.

Apps that crash or don’t function properly can be frustrating to users and are certainly
troublesome for developers.

One of the most problematic messages presented to users and programmers on Linux environments is the all-too-familiar “Segmentation Fault.”

This may not be the time to panic, but it can only be resolved easily if you know how to fix a segmentation fault on Linux.

Comparison Table

[amazon box=”B0013JLRH2,B007QO8SA2,1719439559,1484964098,1441917470,1788993861″ template=”table”]

What Is a Segmentation Fault?

A segmentation fault – also abbreviated as segfault – is actually an error related to memory usage. It can mean that your program performed an invalid memory function due to:

  • A memory address that does not exist
  • A segment of memory that your program does not have authority to
  • A program in a library you’re using has accessed memory that it does not have access to
  • Attempts to write to memory owned by the operating system

Operating systems such as Linux normally divide system memory into segments. The operating system allocates these segments for use by system functions, as well as making memory available for user-written applications.

When a program attempts to access a segment that it does not have rights to, or reads or writes to a non-existent memory address, the fault occurs.

The challenge is finding the cause of the segmentation fault, and fixing it.

Common causes of segmentation faults include:

  • Exceeding the boundary of an array, resulting in a buffer overflow
  • Accessing a memory segment that has been deleted
  • Referencing memory that has not been allocated for your use
  • Attempting to write to read-only memory
  • Address pointers that are initialized to null values being dereferenced

Operating systems such as Linux and Unix incorporate memory management techniques that detect such violations of memory use and throw a signal (SIGSEGV, or segmentation violation) to the program that initiated the fault, resulting in your application receiving the dreaded segmentation fault notification.

[amazon link=”B0013JLRH2″ title=”Algorithms and Programming” /]

[amazon box=”B0013JLRH2″]

[amazon link=”B007QO8SA2″ title=”Programming Problems” /]

[amazon box=”B007QO8SA2″]

Causes of Segmentation Faults

The causes and conditions under which segmentation faults take place vary depending on the operating system and even the hardware applications are running on. Underlying
operating system code can detect and recover from some wayward addressing errors created by application errors, isolating system memory from unauthorized destruction or access caused by buffer overflows or inadvertent programming errors.

Part of the problem in dealing with and resolving segmentation faults on Linux is finding the root cause of the problem. Often segfaults happen as a result of application errors that occurred earlier in an application, such as compromising a memory address that will be referenced later in the program.

In such conditions, the segmentation fault can be encountered when the address is utilized, but the cause is in a different area of the program. Backtracking through the
functionality of the program is often the only way to determine the actual cause of the error.

This is especially true when the segmentation fault presents itself intermittently, which may indicate that there is a relationship with a particular segment of programming code that is encountered only under specific conditions.

Often one of the most challenging factors in isolating the cause of a segmentation fault is in reproducing the error in a consistent manner before you can fix the cause of the fault.

Your Best Way to Fix Segmentation Faults On Linux

Your most foolproof method of fixing segmentation faults is to simply avoid them. This may not always be an option, of course.

If you’re running packaged software or applications that you have downloaded from the internet or provided by a friend or business associate, you may be out of luck. One option for packaged software is to submit a problem report to the vendor or supplier, and hope they will provide a solution or fix the problem through an update or replacement.

As a programmer, you should adhere to best practices in memory management:

  • Keep close track of memory allocation and deletion
  • Diagnose problems thoroughly through adequate testing of all programs and sub-programs
  • Utilize tools for debugging that can help you determine the true cause of the segmentation fault

Trouble-shooting memory violations that cause segfault issues can be tricky, without the use of a good debugger, since the code that caused the memory issue may be in a totally different section of your code from where the segmentation fault crashes the program.

Some compilers will detect invalid access to memory locations such as writing to read-only memory, indicating an error that can be corrected before the program is utilized for testing or production use. Unfortunately, there are also compilers that will not highlight such coding errors or will allow the creation of the executable code despite these errors. The addressing error will not be noted until the program is run, and the segmentation fault rears its ugly head.

Debugging can help you locate the exact section or line of code that is causing the error.

[amazon link=”1719439559″ title=”Fundamentals of Programming Terms and Concepts” /]

[amazon box=”1719439559″]

[amazon link=”1484964098″ title=”Programming Problems 2″ /]

[amazon box=”1484964098″]

Finding and Fixing Segmentation Faults On Linux

The typical action that Linux-based systems take in the event of a segmentation fault is to terminate the execution of the offending process that initiated the condition. Along with halting the program or process, a core file or core dump will often be generated, which is an important tool in debugging the program or finding the cause of the segfault.

Core dumps are valuable in locating specific information regarding the process that was running when the segmentation fault occurred:

  • Snapshot of program memory at the time of termination
  • Program and stack pointers
  • Processor register content
  • Additional useful memory management and OS information

When system-generated core dumps do not provide adequate information for locating the cause of the problem, you can also force dumps at points in your code, to get an exact picture of addresses and memory content at
any point during execution.

Fixing the Segmentation Fault

Sooner or later, every programmer will encounter a program that produces a segmentation fault, requiring some level of debugging to find the source of the error. There are several ways to go about some simple troubleshooting and debugging of a program:

  • Make assumptions about what the program is doing at the point of the segfault, guess what the problem is, and attempt to fix the code to resolve the problem (not very scientific or reliable).
  • Change the program to list variables at strategic points, to help pinpoint the issue.
  • Utilizing a debugging tool to trap the details of program execution and really nail down the exact cause of the segmentation fault.

What makes the most sense to you? Using a debugger, of course.

GDB is a debugging tool available for Unix-type systems and can be a valuable tool in your programming arsenal. With GDB functions you are able to pinpoint the exact location in your programs where segmentation faults are generated and backtrack to the
root cause with minimal time and effort. GDB functionality includes many important functions:

Start your program under GDB control – now the debugger is running behind the scenes, tracking each step of execution. When the segfault takes place, the debugger supplies you with an abundance of valuable information:

  • The line of code where the fault took place
  • Details of the program code being executed

Now you have a good clue as to where the problem is, but how did the program get to that point, and what was information was it working with?

Simply tell the debugger to backtrace, and you will have even more information presented:

  • The methods that called this statement
  • Parameters that were passed
  • Variables in use

So now you know how the program got to the point of the segfault, but perhaps not enough to resolve the problem. This is where additional functions of the debugger come into play for additional troubleshooting steps.

You can set breakpoints in your program so that GDB will stop execution at exactly that point of failure in your logic, allowing you to display what was in variables and memory addresses when that breakpoint is reached. Breakpoints can even include conditions, such that you can break only under specific circumstances.

If that’s not quite enough to identify the problem, set the breakpoint a little earlier in your logic, then tell the debugger to “step” through the logic one line at a time, evaluating the variables and memory constants at each step, until you identify exactly where the unexpected values appear.

[amazon link=”1441917470″ title=”Algorithms and Programming” /]

[amazon box=”1441917470″]

[amazon link=”1788993861″ title=”The Modern C++ Challenge” /]

[amazon box=”1788993861″]

Following the debugging process through your program will nearly always pinpoint the problem that is the root cause of your segmentation faults.

Be certain to follow best practices in your Linux application programming development. Combining good memory management techniques with sophisticated debugging tools will allow you to produce reliable, fault-free programs for use in Linux environments.

В этом руководстве объясняется, как анализировать сообщение segfault в файле messages и выявлять проблему на стороне приложения или операционной системы.

Что такое «segfault»?

Ошибка сегментации (в оригинале segmentation fault часто сокращаемая до segfault) или нарушение доступа – это ошибка, возникшая у оборудования с защитой памяти, чтобы уведомить операционную систему (ОС) о нарушении доступа к памяти.

Ядро Linux отвечает на него, выполняя некоторые корректирующие действия, обычно передавая ошибку в процесс-нарушитель, посылая процессу сигнал типа # 11.

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

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

Как проверить?

1. Определить segfault

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

Это не означает ошибку ядра Linux.

Ядро просто обнаруживает ошибку процесса или программы и (на некоторых архитектурах) выводит информацию в журнал, как показано ниже:

kernel: login[118125]: segfault at 0 ip 00007f4e4d5334a8 sp 00007fffe9177d60 error 15 in pam_unity_uac.so[7f4e4d530000+b000]
kernel: crond[16398]: segfault at 14 ip 00007fd612c128f2 sp 00007fff6a689010 error 4 in pam_seos.so[7fd612baf000+f5000]
kernel: crond[17719]: segfault at 14 ip 00007fd612c128f2 sp 00007fff6a689010 error 4 in pam_seos.so[7fd612baf000+f5000

 2. Что означает подробности этого сообщения?

Значение RIP – это значение регистра указателя команды, а RSP – значение регистра указателя стека.

Значением ошибки является битовая маска битов кода ошибки страницы (из arch/x86/mm/fault.c):

* bit 0 == 0: no page found 1: protection fault
* bit 1 == 0: read access 1: write access
* bit 2 == 0: kernel-mode access 1: user-mode access
* bit 3 == 1: use of reserved bit detected
* bit 4 == 1: fault was an instruction fetch

Вот определение бита ошибки:

enum x86_pf_error_code {
PF_PROT = 1 << 0,
PF_WRITE = 1 << 1,
PF_USER = 1 << 2,
PF_RSVD = 1 << 3,
PF_INSTR = 1 << 4,
};

Код ошибки 15 – 1111 бит.

Наконец, мы можем узнать значение 1111 следующим образом:

01111
^^^^^
||||+---> bit 0
|||+----> bit 1
||+-----> bit 2
|+------> bit 3
+-------> bit 4

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

The interesting error codes from Linux program segfault kernel messages

February 10, 2018

When I wrote up what the Linux kernel’s messages about segfaulting
programs mean, I described what went
into the ‘error N‘ codes and how to work out what any particular
one meant, but I didn’t inventory them all. Rather than put myself
through reverse engineering what any particular error code means,
I’m going to list them all here, in ascending order.

The basic kernel message looks like this:

testp[9282]: segfault at 0 ip 0000000000401271 sp 00007ffd33b088d0 error 4 in testp[400000+98000]

We’re interested in the ‘error N‘ portion, and a little bit
in the ‘at N‘ portion (which is the faulting address).

For all of these, the fault happens in user mode so I’m not going
to mention it specifically for each one. Also, the list of potential
reasons for these segfaults is not exhaustive or fully detailed.

  • error 4: (Data) read from an unmapped area.

    This is your classic wild pointer read. On 64-bit x86, most of the
    address space is unmapped so even a program that uses a relatively
    large amount of memory is hopefully going to have most bad pointers
    go to memory that has no mappings at all.

    A faulting address of 0 is a NULL pointer and falls into page
    zero
    , the lowest page in memory. The kernel prevents people
    from mapping page zero, and in general
    low memory is never mapped, so reads from small faulting addresses
    should always be error 4s.

  • error 5: read from a memory area that’s mapped but not readable.

    This is probably a pointer read of a pointer that is so wild that
    it’s pointing somewhere in the kernel’s area of the address space.
    It might be a guard page,
    but at least some of the time mmap()‘ing things with PROT_NONE
    appears to make Linux treat them as unmapped areas so you get
    error code 4 instead. You might think this could be an area
    mmap()‘d with other permissions but without PROT_READ, but
    it appears that in practice other permissions imply the
    ability to read the memory as well.

    (I assume that the Linux kernel is optimizing PROT_NONE mappings
    by not even creating page table entries for the memory area, rather
    than carefully assembling PTEs that deny all permissions. The error
    bits come straight from the CPU, so if there are no PTEs the CPU says
    ‘fault for an unmapped area’ regardless of what Linux thinks and will
    report in, eg, /proc/PID/maps.)

  • error 6: (data) write to an unmapped area.

    This is your classic write to a wild or corrupted pointer, including
    to (or through) a null pointer. As with reads, writes to guard pages
    mmap()‘d with PROT_NONE will generally show up as this, not as
    ‘write to a mapped area that denies permissions’.

    (As with reads, all writes with small faulting addresses should be
    error 6s because no one sane allows low memory to be mapped.)

  • error 7: write to a mapped area that isn’t writable.

    This is either a wild pointer that was unlucky enough to wind up
    pointing to a bit of memory that was mapped, or an attempt to
    change read-only data, for example the classical C mistake of
    trying to modify a string constant (as seen in the first entry). You might also be trying to
    write to a file that was mmap()‘d read only, or in general
    a memory mapping that lacks PROT_WRITE.

    (All attempts to write to the kernel’s area of address space
    also get this error, instead of error 6.)

  • error 14: attempt to execute code from an unmapped area.

    This is the sign of trying to call through a mangled function
    pointer (or a NULL one), or perhaps returning from a call when
    the stack is in an unexpected or corrupted state so that the
    return address isn’t valid. One source of mangled function pointers
    is use-after-free issues where the (freed) object contains embedded
    function pointers.

    (Error 14 with a faulting address of 0 often means a function
    call through a NULL pointer, which in turn often means ‘making
    an indirect call to a function without checking that it’s defined’.
    There are various larger scale causes of this in code.)

  • error 15: attempt to execute code from a mapped memory area that
    isn’t executable.

    This is probably still a mangled function pointer or return
    address, it’s just that you’re unlucky (or lucky) and there’s
    mapped memory there instead of nothing.

    (Your code could have confused a function pointer with a data
    pointer somehow, but this is a lot rarer a mistake than confusing
    writable data with read-only data.)

If you’re reporting a segfault bug in someone else’s program, the
error code can provide useful clues as to what’s wrong. Combined
with the faulting address and the instruction pointer at the time,
it might be enough for the developers to spot the problem even
without a core dump. If you’re debugging your own programs, well,
hopefully you have core dumps; they’ll give you a lot of additional
information (starting with a stack trace).

(Now that I know how to decode them, I find these kernel messages
to be interesting to read just for the little glimpses they give
me into what went wrong in a program I’m using.)

On 64-bit x86 Linux, generally any faulting address over 0x7fffffffffff
will be reported as having a mapping and so you’ll get error codes
5, 7, or 15 respective for read, write, and attempt to execute.
These are always wild or corrupted pointers (or addresses more
generally), since you never have valid user space addresses up
there.

A faulting address of 0 (sometimes printed as ‘(null)‘, as covered
in the first entry) is a NULL pointer itself. A faulting address
that is small, for example 0x18 or 0x200, is generally an offset
from a NULL pointer. You get these offsets if you have a NULL pointer
to a structure and you try to look at one of the fields (in C,
sptr = NULL; a = sptr->fld;‘), or you have a NULL pointer to an
array or a string and you’re looking at an array element or a
character some distance into it. Under some circumstances a very
large address, one near 0xffffffffffffffff (the very top of memory
space), can be a sign of a NULL pointer that your code then subtracted
from.

(If you see a fault address of 0xffffffffffffffff itself, it’s
likely that your code is treating -1 as a pointer or is failing to
check the return value of something that returns a pointer or ‘(type
*)-1’ on error. Sadly there are C APIs that are that perverse.)

В предыдущей статье описывающей что означают сообщения ядра Linux о segfault программах, было описано, что входит в коды ‘error N’ и как выяснить, что означает тот или иной код. Вместо того, чтобы разбираться, что означает тот или иной код ошибки, в данном статье перечислены здесь, в порядке возрастания.

Основное сообщение ядра выглядит следующим образом:

testp[1234]: segfault at 0 ip 0000000000401271 sp 00007ffd33b088d0 error 4 in testp[400000+98000].

Нас интересует часть ‘error N’, и немного часть ‘at N’ (которая является адресом неисправности).

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

Содержание

  1. error 4: (Данные) прочитаны из не отображенной области.
  2. error 5: чтение из области памяти, которая отображена, но не доступна для чтения.
  3. error 6: (данные) запись в неотмеченную область.
  4. error 7: запись в сопоставленную область, которая недоступна для записи.
  5. error 14: попытка выполнить код из не отображенной области.
  6. error 15: попытка выполнить код из неисполняемой области памяти.
  7. Выводы

error 4: (Данные) прочитаны из не отображенной области.

Это классическое чтение по произвольному указателю. На 64-битном x86 большая часть адресного пространства не отображена, поэтому даже в программе, использующей относительно большой объем памяти, большинство плохих указателей будут попадать в память, которая вообще не имеет отображений.

Ошибочный адрес 0 является NULL-указателем и попадает в нулевую страницу, самую нижнюю страницу в памяти. Ядро не позволяет людям отображать нулевую страницу, и в целом низкая память никогда не отображается, поэтому чтение с маленьких ошибочных адресов всегда должно быть ошибкой 4s.

error 5: чтение из области памяти, которая отображена, но не доступна для чтения.

Вероятно, это чтение указателя, который настолько произвольный, что указывает куда-то в область адресного пространства ядра. Это может быть защитная страница, но, по крайней мере, в некоторых случаях mmap()’ing вещи с PROT_NONE, кажется, заставляет Linux рассматривать их как неотмеченные области, так что вы получаете код ошибки 4 вместо этого.

Вы можете подумать, что это может быть область mmap()’d с другими разрешениями, но без PROT_READ, но, похоже, что на практике другие разрешения подразумевают возможность чтения памяти.

(Я предполагаю, что ядро Linux оптимизирует отображения PROT_NONE, даже не создавая записей в таблице страниц для этой области памяти, а не тщательно собирая PTE, которые запрещают все разрешения. Биты ошибки приходят прямо от CPU, так что если нет PTE, CPU говорит «ошибка из-за неразмеченной области», независимо от того, что Linux думает и сообщает, например, в /proc/PID/maps).

error 6: (данные) запись в неотмеченную область.

Это классическая запись в случайны или поврежденный указатель, в том числе в нулевой указатель (или через него). Как и при чтении, запись на защитные страницы mmap()’d с PROT_NONE будет обычно проявляться именно так, а не как «запись в отображенную область, на которую запрещены разрешения».

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

error 7: запись в сопоставленную область, которая недоступна для записи.

Это либо «случайный» указатель, которому не повезло попасть на часть памяти, которая была отображена, либо попытка изменить данные, доступные только для чтения, например, классическая ошибка языка Си — попытка изменить строковую константу (как показано в первой записи). Вы также можете пытаться записать в файл, который был создан mmap() только для чтения, или вообще в память, в которой отсутствует PROT_WRITE.

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

error 14: попытка выполнить код из не отображенной области.

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

(Ошибка 14 с ошибочным адресом 0 часто означает вызов функции через указатель NULL, что, в свою очередь, часто означает «косвенный вызов функции без проверки ее определения». В коде есть различные более масштабные причины этого).

error 15: попытка выполнить код из неисполняемой области памяти.

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

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

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

Выводы

В 64-битном x86 Linux, как правило, любой неисправный адрес больше 0x7fffffffffff сообщается как имеющий отображение, и поэтому вы получите коды ошибок 5, 7 или 15, соответствующие для чтения, записи и попытки выполнения. Это всегда случайные или поврежденные указатели (или адреса в более общем случае), поскольку у вас никогда не будет действительных адресов пространства пользователя.

Ошибочный адрес 0 (иногда печатается как ‘(null)’, как описано в первой записи) сам по себе является указателем NULL. Маленький ошибочный адрес, например 0x18 или 0x200, обычно является смещением от указателя NULL. Вы получаете эти смещения, если у вас есть NULL-указатель p

Понравилась статья? Поделить с друзьями:

Читайте также:

  • Sega library internal error
  • Seek unknown error lba 0
  • Seek timeout error victoria
  • Seek error rate что это seagate
  • Seek error rate перевод

  • 0 0 голоса
    Рейтинг статьи
    Подписаться
    Уведомить о
    guest

    0 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии