TL; DR: проблема в том, что GDB не поддерживает внутренние сигналы musl, о которых сообщается в этом билете gdb .
Быстрый и грязный исправленный GDB доступен здесь:
https://github.com/shaharv/alpine-gdb-builds/releases/tag/v0.1
Патч-коммит: shaharv / binutils-gdb @ 0ca9c66 .
С патчем сигнал правильно идентифицируется как SIGSYNCCALL.
Тогда его можно замаскировать, используя handle SIGSYNCCALL nostop noprint pass
.
К счастью, я смог найти обходной путь!
Сбой GDB при отладке Java Alpine OpenJDK можно обойти следующим образом:
- Start GDB
break os::init_2
- Запустите Java с нужными аргументами командной строки
- Когда достигается точка останова,
set MaxFDLimit=0
- Продолжить и отладить как обычно.
Я тестировал обходной путь с ранним доступом OpenJDK 8 и 11, поэтому он, вероятно, будет работать и с OpenJDK 9 и 10.
К сожалению, область применения этого обходного пути очень ограничена:
- Это работает, только если JDK имеет символы отладки - будь то локальная отладочная сборка OpenJDK или использующий пакет
openjdk8-dbg
отладочных символов.
- Он подходит только для командной строки GDB и не будет работать с внешними интерфейсами GDB, такими как CLion и Eclipse CDT.
Резюме:
Сбой происходит, когда внутри GDB вызывается функция setrlimit
. Реализация musl setrlimit
сигнализирует потоки с SIGSYNCCALL
, который не поддерживается gdb, и приводит к ошибке Unknown signal
. Во избежание ошибки соответствующий код инициализации JavaMain
отключается отключением глобальной переменной MaxFDLimit
.
Полное объяснение:
Во время инициализации JVM создается собственный поток JavaMain
, который создает виртуальную машину. Во время создания виртуальной машины происходит специфическая инициализация ОС, в которой вызывается setrlimit
. Вот соответствующая часть трассировки стека:
#0 __synccall (func=func@entry=0x7ffff7da2662 <do_setrlimit>, ctx=ctx@entry=0x7ffff7ff4720) at src/thread/synccall.c:48
#1 0x00007ffff7da26a1 in setrlimit (resource=resource@entry=7, rlim=rlim@entry=0x7ffff7ff4750) at src/misc/setrlimit.c:42
#2 0x00007ffff73bd1fe in os::init_2 ()
at /home/buildozer/aports/community/openjdk8/src/icedtea-3.8.0/openjdk/hotspot/src/os/linux/vm/os_linux.cpp:5096
#3 0x00007ffff746177d in Threads::create_vm (args=0x7ffff7ff4a20, canTryAgain=canTryAgain@entry=0x7ffff7ff4987)
at /home/buildozer/aports/community/openjdk8/src/icedtea-3.8.0/openjdk/hotspot/src/share/vm/runtime/thread.cpp:3361
#4 0x00007ffff729cd48 in JNI_CreateJavaVM (vm=0x7ffff7ff4a10, penv=0x7ffff7ff4a18, args=<optimized out>)
at /home/buildozer/aports/community/openjdk8/src/icedtea-3.8.0/openjdk/hotspot/src/share/vm/prims/jni.cpp:5221
#5 0x00007ffff7b61b0b in InitializeJVM (ifn=<synthetic pointer>, penv=0x7ffff7ff4a18, pvm=0x7ffff7ff4a10)
at /home/buildozer/aports/community/openjdk8/src/icedtea-3.8.0/openjdk/jdk/src/share/bin/java.c:1231
#6 JavaMain (_args=<optimized out>)
Виновной является вызов функции setrlimit
.
Реализация musl setrlimit
является AS-Safe , то есть ее можно вызывать из асинхронных обработчиков сигналов. Часть синхронизации обрабатывается путем вызова __synccall
( setrlimit.c ):
int setrlimit(int resource, const struct rlimit *rlim)
{
struct ctx c = { .res = resource, .rlim = rlim, .err = -1 };
__synccall(do_setrlimit, &c);
if (c.err) {
if (c.err>0) errno = c.err;
return -1;
}
return 0;
}
__synccall
( synccall.c ) блокирует все сигналы, затем выполняет итерацию всех потоков процесса и отправляет им сигнал SIGSYNCCALL
(и только когда все потоки подтверждают сигнал, do_setrlimit
выполнено):
r = -__syscall(SYS_tgkill, pid, tid, SIGSYNCCALL);
Однако сигнал SIGSYNCCALL
является внутренним для musl и не обрабатывается GDB. GDB явно обрабатывает все типы сигналов, но SIGSYNCCALL
не включен в обработанные сигналы (см. сигналов GDB. C ). Поэтому, когда сигнал повышается, GDB завершается с ошибкой Unknown signal
.
Обход:
Обходной путь - отключение вызова на setrlimit
в OpenJDK на лету. Соответствующий код находится в функции os::init_2
( os_linux.cpp ):
if (MaxFDLimit) {
// set the number of file descriptors to max. print out error
// if getrlimit/setrlimit fails but continue regardless.
struct rlimit nbr_files;
int status = getrlimit(RLIMIT_NOFILE, &nbr_files);
if (status != 0) {
if (PrintMiscellaneous && (Verbose || WizardMode))
perror("os::init_2 getrlimit failed");
} else {
nbr_files.rlim_cur = nbr_files.rlim_max;
status = setrlimit(RLIMIT_NOFILE, &nbr_files);
if (status != 0) {
if (PrintMiscellaneous && (Verbose || WizardMode))
perror("os::init_2 setrlimit failed");
}
}
}
При установке MaxFDLimit
в 0 приведенный выше код не выполняется, и инициализация ВМ может продолжаться в обычном режиме. Существует опция командной строки для переключения этой переменной, -XX:-MaxFDLimit
, но она доступна на только для Solaris , поэтому у нас нет выбора, кроме как отключить эту переменную вручную внутри gdb.
Причина, по которой стоит MaxFDLimit
, является исторической и предназначалась для увеличения предела по умолчанию для файловых дескрипторов в старых системах, которые имели очень низкий предел FD по умолчанию (256), как описано в JDK-8010126 . Alpine V3.8 имеет ограничение по умолчанию 1024, поэтому можно безопасно отключить этот код - и, если необходимо, ограничения можно увеличить с помощью ulimit
, а не самой JVM.