image

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

Сигналы SIGSEGV возникают на уровне операционной системы, но столкнуться с ними также вполне можно и в контексте контейнерных технологий, например, Docker и Kubernetes. Когда контейнер завершает работу, выдав код возврата 139, дело именно в том, что он получил сигнал SIGSEGV. Операционная система завершает процесс контейнера, чтобы предохраниться от нарушения целостности памяти.

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

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

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


Термин ошибка сегментирования может показаться туманным, но с технической точки зрения это очень простое явление. Вот в чём оно заключается: процесс получает сигнал SIGSEGV из-за того, что попытался прочитать информацию из такой области памяти, к которой ему не разрешено обращаться – или записать информацию в такую область. Как правило, ядро завершает такой процесс, чтобы избежать повреждения памяти. Данное поведение можно изменить, явно обрабатывая сигнал в коде программы.

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

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

Вот тривиальный пример программы на C, в которой фигурирует ошибка сегментирования:

int main() {
  char *buffer;
  buffer[0] = 0;
  return 0;
}

Сохраним программу как hello-world.c и скомпилируем её при помощи make:

$ make hello-world

Теперь выполним скомпилированный бинарник:

$ ./hello-world
Segmentation fault (core dumped)

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

$ echo $?
139

Почему так происходит? В программе была создана переменная buffer, но под неё не было выделено памяти. В результате присваивания buffer[0] = 0 происходит запись в невыделенную память. Можно исправить программу, гарантировав, что размера буфера точно хватит, чтобы в нём поместились все требуемые данные:

int main() {
  char *buffer[1];
  buffer[0] = 0;
  return 0;
}

Если выделить буфер buffer размером 1 байт, то этой памяти точно хватит, чтобы обработать присвоенное значение. Эта программа успешно завершается с кодом возврата 0.

Ошибки сегментирования в контейнерах


Теперь давайте рассмотрим, что случится, если ошибка сегментирования произойдёт в контейнере. Вот простой файл Dockerfile для показанного выше приложения, которое аварийно завершилось:

FROM alpine:latest
RUN apk install --upgrade build-base
COPY hello-world.c .
RUN make hello-world && mv hello-world /usr/bin/hello-world
CMD ["hello-world"]

Соберём образ нашего контейнера при помощи следующей команды:

$ docker build -t segfault:latest .

Теперь запустим контейнер:

$ docker run segfault:latest

Контейнер запустится, выполнит команду и сразу же завершит работу. Воспользуйтесь docker ps с флагом -a, чтобы извлечь подробную информацию об остановленном контейнере:

$ docker ps -a


image

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

Отладка ошибок сегментирования в Kubernetes


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

apiVersion: v1
kind: Pod
metadata:
  name: segfault
spec:
  containers:
	- name: segfault
	image: segfault:latest

При помощи kubectl добавьте под в ваш кластер:

$ kubectl apply -f pod.yaml

Теперь давайте вытащим детали пода:
$ kubectl get pod/segfault


image

Под то и дело валится в цикле перезапуска. При помощи команды describe выясним, в чём же дело:

$ kubectl describe pod/segfault
Name: segfault
Namespace: default
...
Containers:
  segfault:
	...
	Last State:   Terminated
    Reason: 	Error
	Exit Code:  139

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

Справляемся с ошибками сегментирования


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

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

Выявление проблемного кода


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

$ docker logs my-container
 
$ kubectl logs pod/my-pod

Ориентируйтесь на то, что происходит в контейнере, чтобы самостоятельно докопаться до источника ошибки. Что это – обращение к массиву, ссылка на указатель, незащищённая запись в область памяти? Или какая-то другая проблема?

Несовместимость с окружением


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

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

Изредка могут возникать неискоренимые ошибки сегментирования, которые ничем не удаётся объяснить. Возможно, в таких случаях дело в несовместимости с железом конкретной машины. Такие ошибки также могут быть симптомом отказа памяти. Именно в контексте работы с кластером Kubernetes (работающим в инфраструктуре публичного провайдера K8s) такие проблемы маловероятны. Попробуйте выполнить memtester и исключить возможные аппаратные проблемы, которые можно решить, правильно организовав поддержку железа.

Прицельная отладка


В Linux есть инструменты, при помощи которых можно адресно отлаживать сигналы SIGSEGV. Любые ошибки сегментирования всегда отражаются в сообщениях из логов ядра. Поскольку контейнеры выполняются как процессы в ядре вашего хоста, эти записи будут выводиться, даже если ошибка произошла внутри контейнера.
Чтобы изучить системный лог, достаточно просмотреть содержимое /var/log/syslog:

$ sudo tail -f /var/log/syslog

Эта команда выдаст непрерывный поток логов в консоль, и они будут выводиться, пока вы сами не отмените эту операцию при помощи Ctrl+C. Теперь попытайтесь воспроизвести то событие, из-за которого возникла ошибка сегментирования. Сигнал SIGSEGV будет выглядеть в логе вот так:

hello-world[2631584]: segfault at 7f4624c6cfe0 ip 000055730c3621ed sp 00007ffce90e35f0 error 7 in hello-world[55730c362000+1000]

Вот как можно интерпретировать этот лог:

  • at <_address_>: запрещённый адрес в памяти, к которому пытается обратиться ваш код.
  • ip <_pointer_>: тот адрес в памяти, по которому находится код-нарушитель.
  • sp <_pointer_>: указатель стека для данной операции, из которого мы узнаём адрес последнего запроса, сделанного программой в этом стеке.
  • error <_code_>: по коду ошибки определяем, операцию какого типа попыталась совершить программа. Среди распространённых кодов – следующие: 6 (запись в невыделенную область); 7 (запись в область, которая доступна для чтения, но не для записи); 4 (чтение из невыделенной области) и 5 (чтение из области, предназначенной только для записи).

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

Аккуратная обработка ошибок сегментирования


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

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

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

Заключение


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



Возможно, захочется почитать и это:



Комментарии (8)


  1. iig
    24.09.2023 20:43

    Если выделить буфер buffer размером 1 байт

    int main() {
      char *buffer[1];
      buffer[0] = 0;
      return 0;
    }
    

    Зануда говорит, что тут выделяется не 1 байт. Вам повезло ;)


    1. datacompboy
      24.09.2023 20:43

      Так и записывают в указатель, а не символ ????


  1. VADemon
    24.09.2023 20:43
    +4

    Вот тривиальный пример программы на C, в которой фигурирует ошибка сегментирования:

    Ой да автор... Это тривиальный пример программы с undefined behavior, а не с гарантированным segmentation fault. Дальше ещё не читал, надеюсь примеры подобного качества на Си закончатся.


    1. vassabi
      24.09.2023 20:43

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


      1. AlexSky
        24.09.2023 20:43
        +4

        Поддержу автора предыдущего коммента. UB не обязательно станет сегфолтом. Может"повезти", и в указателе окажется память, к которой есть доступ.


        1. vassabi
          24.09.2023 20:43

          (me: думает - при каких ключах компилятора и линкера в том указателе будет память с доступом на запись)

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


  1. iig
    24.09.2023 20:43
    +4

    Жаль, что я так и не понял, как побороть SIGSEGV в чужой программе. И чем отличается SIGSEGV в контейнере и в просто запущенной программе тоже не понял. Ну да, перевод же, уточнений не будет.


    1. slonopotamus
      24.09.2023 20:43
      +1

      чем отличается SIGSEGV в контейнере и в просто запущенной программе

      Спойлер: ничем