Цель этого поста — дать тебе общее руководство по включению и сбору core dumps для приложений, работающих в docker контейнере.
Настраиваем хост для сохранения дампов
Для начала надо сконфигурировать хостовую машину, чтобы сохранять такие дампы в нужное место. Нет, не в жопу.
ㅤ
Универсальный способ — задать шаблон core pattern. Шаблон определяет путь и информацию о процессе который наебнулся.
echo '/tmp/core.%e.%p' | sudo tee /proc/sys/kernel/core_pattern
Кратенько:
%e — имя процесса %p — pid процессаБолее подробно о конфигурации core pattern можешь почитать в man-странице ядра Linux.
Как вариант, можно настроить host изнутри контейнера через CMD или ENTRYPOINT. Но контейнер в этом случае должен запускаться в privileged mode, что хуева с точки зрения безопасности.
Пример хуёвого приложения
#include <cstdlib>
void foo() {
std::abort();
}
int main() {
foo();
return 0;
}
После компиляции и запуска, приложение наебнется с ошибкой.
Пишем Dockerfile для этого приложения
FROM ubuntu:22.04
# Install tools
RUN apt update \
&& apt -y install \
build-essential \
gdb \
&& rm -rf /var/lib/apt/lists/*
# Build the application
COPY ./ /src/
WORKDIR /src/
RUN g++ main.cpp -o app
CMD ["/src/app"]
Тот кто в теме, сможет его прочитать и понять. Если не понятно, гугли и разбирай, качнешь свой девопсовый скиллз.
Запускаем контейнер с нужными опциями:
docker run \
--init \
--ulimit core=-1 \
--mount type=bind,source=/tmp/,target=/tmp/ \
application:latest
Разбираем опции:
--init — гарантирует корректную обработку сигналов внутри контейнера--ulimit — устанавливает неограниченный размер core dump для процессов внутри контейнера--mount — монтирует /tmp из хоста в контейнер, чтобы дампы, создаваемые внутри контейнера, были доступны после его остановки или удаленияЗдесь важно: путь
source на хосте должен совпадать с тем, который задан в шаблоне core pattern.После того как приложение наебнется, core dump будет сохранён на хостовой машине в директории
/tmp. ls /tmp/core*
# /tmp/core.app.5
Анализируем дамп с помощью gdb
Такс, мы получили core dump и он у нас лежит на хостовой машине, но рекомендуется открыть его внутри контейнера. Контейнер должен быть из того же образа, в котором компилилось приложение.
Это поможет убедиться, что все зависимости (библиотеки и прочая хуитень) доступны.
docker run \
-it \
--mount type=bind,source=/tmp/,target=/tmp/ \
application:latest \
bash
Если в образе нет исходного кода, можно допом смаунтить исходники:
docker run \
-it \
--mount type=bind,source=/tmp/,target=/tmp/ \
--mount type=bind,source=<путь_к_исходникам_на_хосте>,target=/src/ \
application:latest \
bash
Теперь внутри контейнера запускаем:
gdb app /tmp/core.app.5
После загрузки дампа можно выполнить команду
bt (backtrace), чтобы увидеть трассировку стека:(gdb) bt
#0 __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:51
#1 0x00007f263f378921 in __GI_abort () at abort.c:79
#2 0x000055f9a9d16653 in foo() ()
#3 0x000055f9a9d1665c in main ()
Команда поможет определить, где именно произошел факап.
Давай быстренько разберем ошибку.
#0 и #1 показывают, что процесс получил сигнал 6 (SIGABRT) и завершился через abort()#2 — вызов произошёл внутри функции foo()#3 — main() вызвал foo()В исходнике было:
void foo() {
std::abort();
}То есть ошибка здесь не баг компиляции или рантайма, а намеренно вставленный вызов
std::abort(), который и приводит к аварийному завершению и генерации core dump.Если у тебя docker-compose, то все флаги (
--init, --ulimit, --mount и т.д.) применимы и для него. То есть отладку можно легко адаптировать.Хуй знает чё еще написать, завтра тему дебага продолжим, чет в конце года много траблшутинга навалило разнообразного.
🛠 #linux #debug #dev
—
✅ @bashdays ✅ @linuxfactory ✅ @blog