Сигнал 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
Здесь мы получаем код возврата 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
Под то и дело валится в цикле перезапуска. При помощи команды
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. Это может свидетельствовать о серьёзных проблемах с безопасностью или надёжностью в вашем коде.
Если такой сигнал просто отловить и проигнорировать, то впоследствии в вашей программе могут возникнуть и другие проблемы. Обычно они сводятся к чтению или записи вне допустимых границ.
❯ Заключение
Ошибки сегментирования возникают, когда программа пытается использовать ту память, доступ к которой ей не разрешён. Также такие ошибки возможны при попытке записать данные в память, предназначенную только для чтения и наоборот. В этой статье было показано, как легко в коде допускаются ошибки, потенциально приводящие к таким проблемам. Ещё мы рассмотрели, как выявлять ошибки сегментирования, возникающие из-за аварийного завершения контейнеров, и как организовать отладку таких ошибок, если подобные проблемы начнут возникать у вас в программе. Если заблаговременно предотвращать подобные ошибки, то ваши приложения будут работать максимально надёжно и практически без перебоев.
Возможно, захочется почитать и это:
- ➤ Запуск приложений на .NET в качестве службы на Linux-системе с systemd
- ➤ Реализуем на Rust пул потоков с балансировкой нагрузки, пользуясь только стандартной библиотекой
- ➤ CloudReady: личные впечатления от облачной ОС
- ➤ Процессор, который использовался в «Тетрисе»: на каких SoC работала недорогая микроэлектроника в 90-х?
- ➤ Dark Souls, Project Dark: семь раз умри, один раз убей
Комментарии (8)
VADemon
24.09.2023 20:43+4Вот тривиальный пример программы на C, в которой фигурирует ошибка сегментирования:
Ой да автор... Это тривиальный пример программы с undefined behavior, а не с гарантированным segmentation fault. Дальше ещё не читал, надеюсь примеры подобного качества на Си закончатся.
vassabi
24.09.2023 20:43хмм ... вы хотите сказать, что одно не является подмножеством другого ?
AlexSky
24.09.2023 20:43+4Поддержу автора предыдущего коммента. UB не обязательно станет сегфолтом. Может"повезти", и в указателе окажется память, к которой есть доступ.
vassabi
24.09.2023 20:43(me: думает - при каких ключах компилятора и линкера в том указателе будет память с доступом на запись)
хм, а ведь действительно - там может быть адрес указателя (например) в сегмент стека.
iig
24.09.2023 20:43+4Жаль, что я так и не понял, как побороть SIGSEGV в чужой программе. И чем отличается SIGSEGV в контейнере и в просто запущенной программе тоже не понял. Ну да, перевод же, уточнений не будет.
slonopotamus
24.09.2023 20:43+1чем отличается SIGSEGV в контейнере и в просто запущенной программе
Спойлер: ничем
iig
Зануда говорит, что тут выделяется не 1 байт. Вам повезло ;)
datacompboy
Так и записывают в указатель, а не символ ????