Linux: обработка ошибки сегментации и получение дампа ядра
Когда мое приложение падает из-за ошибки сегментации, я хотел бы получить дамп ядра из системы. Я делаю это, настраивая перед рукой
ulimit -c unlimited
Я также хотел бы, чтобы в журналах моего приложения была указана ошибка сегментации. Я делаю это с помощью sigaction()
, Однако, если я это сделаю, сигнал не достигнет своей обработки по умолчанию, и дамп ядра не будет сохранен.
Как я могу одновременно выгрузить ядро системы и строку журнала из моего собственного обработчика сигналов?
3 ответа
Ответ: поставь знак с флагом SA_RESETHAND
и просто вернитесь из обработчика. Та же самая инструкция повторяется снова, вызывая ошибку сегментации и вызывая обработчик по умолчанию.
- Перезаписать обработчик сигнала по умолчанию для
SIGSEGV
для вызова вашей пользовательской функции регистрации. - После регистрации восстановите и запустите обработчик по умолчанию, который создаст дамп ядра.
Вот пример программы, использующей signal
:
void sighandler(int signum)
{
myLoggingFunction();
// this is the trick: it will trigger the core dump
signal(signum, SIG_DFL);
kill(getpid(), signum);
}
int main()
{
signal(SIGSEGV, sighandler);
// ...
}
Та же идея должна также работать с sigaction
,
Источник: Как работать с SIGSEGV, но также генерировать дамп ядра
В вашем обработчике сигналов не нужно делать ничего особенного.
Как объяснено в: Куда возвращается обработчик сигнала?по умолчанию программа возвращается к той самой инструкции, которая вызвала SIGSEGV после обработки сигнала.
Кроме того, протестировано в Ubuntu 22.04, поведение по умолчанию дляsignal
заключается в том, что он автоматически отменяет регистрацию обработчика. однако предполагает, что это не очень переносимо, поэтому, возможно, лучше использовать более явный системный вызов.
Поэтому по умолчанию в этой системе происходит следующее:
- сигнал обрабатывается
- обработчик автоматически отключается
- после возврата вы возвращаетесь к инструкции, которая вызывает сигнал
- сигнал повторяется
- обработчика нет, так что сбой происходит точно так же, как если бы мы не обрабатывали сигнал
Самое главное проверить, можете ли вы вообще генерировать дампы ядра независимо от обработчика сигнала. Примечательно, что многие более новые системы, такие как Ubuntu 22.04, имеют сложный обработчик дампа ядра, который предотвращает создание файлов ядра: https://askubuntu.com/questions/1349047/where-do-i-find-core-dump-files-and- как-я-просматриваю-и-анализирую-обратную-улицу-st/1442665#1442665 и которую вы можете деактивировать отдельно с помощью:
echo 'core' | sudo tee /proc/sys/kernel/core_pattern
Минимальный работающий пример:
main.c
#include <signal.h> /* signal, SIGSEGV */
#include <unistd.h> /* write, STDOUT_FILENO */
void signal_handler(int sig) {
(void)sig;
const char msg[] = "signal received\n";
write(STDOUT_FILENO, msg, sizeof(msg));
}
int myfunc(int i) {
*(int *)0 = 1;
return i + 1;
}
int main(int argc, char **argv) {
(void)argv;
signal(SIGSEGV, signal_handler);
int ret = myfunc(argc);
return ret;
}
скомпилировать и запустить:
gcc -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o main.out main.c
./main.out
Выход терминала содержит:
signal received
Segmentation fault (core dumped)
Итак, мы видим, что сигнал был обработан, и мы получили основной файл.
И проверка основного файла с помощью:
gdb main.out core.243260
ставит нас на правильную строку:
#0 myfunc (i=1) at main.c:12
12 *(int *)0 = 1;
поэтому мы вернулись к нему, как и ожидалось.
Сделать его более портативным с помощью
man signal
В разделе переносимости есть Библия текста о том, какsignal()
варьируется в разных ОС и версиях:
Единственное переносимое использование signal() — установить расположение сигнала в SIG_DFL или SIG_IGN. Семантика при использовании signal() для установки обработчика сигнала различается в разных системах (и POSIX.1 явно разрешает это изменение); не используйте его для этой цели.
POSIX.1 решил проблему с переносимостью, указав sigaction(2), который обеспечивает явное управление семантикой при вызове обработчика сигнала; используйте этот интерфейс вместо signal().
В исходных системах UNIX, когда обработчик, установленный с помощью signal(), вызывался при доставке сигнала, расположение сигнала сбрасывалось на SIG_DFL, и система не блокировала доставку дальнейших экземпляров сигнала. Это эквивалентно вызову sigaction(2) со следующими флагами:
sa.sa_flags = SA_RESETHAND | SA_NODEFER;
System V также предоставляет эту семантику для signal(). Это было плохо, потому что сигнал мог быть доставлен снова до того, как обработчик успел восстановиться. Кроме того, быстрая доставка одного и того же сигнала может привести к рекурсивным вызовам обработчика.
BSD улучшила эту ситуацию, но, к сожалению, при этом также изменила семантику существующего интерфейса signal(). В BSD, когда вызывается обработчик сигнала, расположение сигнала не сбрасывается, и дальнейшие экземпляры сигнала блокируются от доставки во время выполнения обработчика. Кроме того, некоторые блокирующие системные вызовы автоматически перезапускаются, если их прерывает обработчик сигнала (см. signal(7)). Семантика BSD эквивалентна вызову sigaction(2) со следующими флагами:
sa.sa_flags = SA_RESTART;
В линуксе ситуация следующая:
Системный вызов signal() ядра обеспечивает семантику System V.
По умолчанию в glibc 2 и более поздних версиях функция-оболочка signal() не вызывает системный вызов ядра. Вместо этого он вызывает sigaction(2), используя флаги, обеспечивающие семантику BSD. Это поведение по умолчанию обеспечивается до тех пор, пока определен подходящий макрос проверки функций: _BSD_SOURCE в glibc 2.19 и более ранних версиях или _DEFAULT_SOURCE в glibc 2.19 и более поздних версиях. (По умолчанию эти макросы определены; подробности см. в feature_test_macros(7).) Если такой макрос тестирования функций не определен, то signal() обеспечивает семантику System V.
Кажется, это предполагает, что я должен получить семантику BSD по умолчанию, но по какой-то причине я, кажется, получаю семантику System V, потому что:
sudo strace -f -s999 -v ./main.out
содержит:
rt_sigaction(SIGSEGV, {sa_handler=0x55b428604189, sa_mask=[], sa_flags=SA_RESTORER|SA_INTERRUPT|SA_NODEFER|SA_RESETHAND|0xffffffff00000000, sa_restorer=0x7fb173d0a520}, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
который имеетSA_NODEFER|SA_RESETHAND
. Примечательно, что флаг, о котором мы заботимся больше всего,SA_RESETHAND
, который сбрасывает обработчик к поведению по умолчанию.
Но, возможно, я просто неверно истолковал один из стихов Священного Текста.
Итак, просто чтобы быть более портативным, мы могли бы сделать то же самое, что и выше, сsigaction
вместо:
sigaction.c
#define _XOPEN_SOURCE 700
#include <signal.h> /* signal, SIGSEGV */
#include <unistd.h> /* write, STDOUT_FILENO */
void signal_handler(int sig) {
(void)sig;
const char msg[] = "signal received\n";
write(STDOUT_FILENO, msg, sizeof(msg));
}
int myfunc(int i) {
*(int *)0 = 1;
return i + 1;
}
int main(int argc, char **argv) {
(void)argv;
/* Adapted from: https://www.gnu.org/software/libc/manual/html_node/Sigaction-Function-Example.html */
struct sigaction new_action;
new_action.sa_handler = signal_handler;
sigemptyset(&new_action.sa_mask);
new_action.sa_flags = SA_NODEFER|SA_RESETHAND;
sigaction(SIGINT, &new_action, NULL);
int ret = myfunc(argc);
return ret;
}
который ведет себя так же, какmain.c
в убунту 22.04.